@stage5/lumine 0.2.92 → 0.2.95
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 +31 -0
- package/lib/admin-daily.js +338 -0
- package/lib/admin.js +544 -126
- package/lib/api.js +58 -0
- package/lib/app-mcp/server.js +135 -23
- package/lib/assets.js +93 -2
- package/lib/commands.js +18 -0
- package/lib/constants.js +5 -0
- package/lib/report-assets/marked.min.js +69 -0
- package/lib/util.js +28 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +6 -4
- package/sdk/LUMINE_ADMIN.md +41 -0
package/lib/admin.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { assertAuthScope, resolveAuth } from "./auth.js";
|
|
14
14
|
import { requestJson } from "./http.js";
|
|
15
|
+
import { formatBytes, parseStorageSizeBytes } from "./util.js";
|
|
15
16
|
import {
|
|
16
17
|
extractNewsClaim,
|
|
17
18
|
readAdminJsonFile,
|
|
@@ -31,6 +32,11 @@ import {
|
|
|
31
32
|
runManagedBuildReview,
|
|
32
33
|
} from "./build-review.js";
|
|
33
34
|
import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
|
|
35
|
+
import {
|
|
36
|
+
dailyRunGather,
|
|
37
|
+
featuredCandidates,
|
|
38
|
+
reportServe,
|
|
39
|
+
} from "./admin-daily.js";
|
|
34
40
|
import {
|
|
35
41
|
readApprovedFeaturedPlan,
|
|
36
42
|
runFeaturedWorkflow,
|
|
@@ -141,6 +147,238 @@ const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
|
|
|
141
147
|
const CHAT_REPORT_STATUSES = ["open", "reviewing", "resolved", "dismissed"];
|
|
142
148
|
const CHAT_REPORT_LIST_STATUSES = ["pending", ...CHAT_REPORT_STATUSES, "all"];
|
|
143
149
|
const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
|
|
150
|
+
// `lumine admin review`: one queue for every Build unlock (XP/Coin rewards,
|
|
151
|
+
// project room, Lumine file storage, AI Card crafting). The per-type commands
|
|
152
|
+
// (reward-review, cardcraft-review, storage) are thin aliases of it.
|
|
153
|
+
const REVIEW_REQUEST_TYPES = [
|
|
154
|
+
"rewards",
|
|
155
|
+
"project-limit",
|
|
156
|
+
"storage-limit",
|
|
157
|
+
"cardcraft",
|
|
158
|
+
];
|
|
159
|
+
const REVIEW_REQUEST_TYPE_ALIASES = {
|
|
160
|
+
rewards: "rewards",
|
|
161
|
+
reward: "rewards",
|
|
162
|
+
"reward-review": "rewards",
|
|
163
|
+
"project-limit": "project-limit",
|
|
164
|
+
project: "project-limit",
|
|
165
|
+
room: "project-limit",
|
|
166
|
+
"storage-limit": "storage-limit",
|
|
167
|
+
storage: "storage-limit",
|
|
168
|
+
cardcraft: "cardcraft",
|
|
169
|
+
"card-craft": "cardcraft",
|
|
170
|
+
crafting: "cardcraft",
|
|
171
|
+
};
|
|
172
|
+
const REVIEW_REQUEST_LIST_STATUSES = [
|
|
173
|
+
"queue",
|
|
174
|
+
"pending",
|
|
175
|
+
"open",
|
|
176
|
+
"approved",
|
|
177
|
+
"rejected",
|
|
178
|
+
"all",
|
|
179
|
+
];
|
|
180
|
+
const REVIEW_REQUEST_LABELS = {
|
|
181
|
+
rewards: "XP & Coin rewards",
|
|
182
|
+
"project-limit": "Project room",
|
|
183
|
+
"storage-limit": "Lumine file storage",
|
|
184
|
+
cardcraft: "AI Card crafting",
|
|
185
|
+
};
|
|
186
|
+
const REVIEW_REQUEST_USAGE =
|
|
187
|
+
"Usage: lumine admin review list [--type rewards|project|storage|cardcraft] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <type:id> [--reason <text>] | revoke <type:id> --reason <text> | propose rewards:<id> --dir <edited-snapshot> --config <rules.json> [--reason <text>]. A bare id works with --type.";
|
|
188
|
+
|
|
189
|
+
function parseReviewRequestType(value) {
|
|
190
|
+
const key = String(value || "")
|
|
191
|
+
.trim()
|
|
192
|
+
.toLowerCase();
|
|
193
|
+
const type = REVIEW_REQUEST_TYPE_ALIASES[key];
|
|
194
|
+
if (!type) {
|
|
195
|
+
throw cliValidationError(
|
|
196
|
+
`Unknown request type "${key}". Use rewards, project, storage or cardcraft.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return type;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// "storage:12", "rewards:70", or a bare id with --type (or a fixed type).
|
|
203
|
+
function parseReviewRequestRef(target, { type: fixedType, typeOption } = {}) {
|
|
204
|
+
const raw = String(target || "").trim();
|
|
205
|
+
const match = /^([a-z-]+):(\d+)$/i.exec(raw);
|
|
206
|
+
if (match) {
|
|
207
|
+
const type = parseReviewRequestType(match[1]);
|
|
208
|
+
if (fixedType && type !== fixedType) {
|
|
209
|
+
throw cliValidationError(
|
|
210
|
+
`This command handles ${REVIEW_REQUEST_LABELS[fixedType]} requests only.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
type,
|
|
215
|
+
id: parseRequiredInteger(match[2], "Request ID", 1),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const type = fixedType || (typeOption ? parseReviewRequestType(typeOption) : "");
|
|
219
|
+
if (!type) {
|
|
220
|
+
throw cliValidationError(
|
|
221
|
+
"Name the request as <type>:<id> (e.g. storage:12) or pass --type.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return { type, id: parseRequiredInteger(raw, "Request ID", 1) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Builds the operation for list/show/approve/reject/revoke. `fixedType` is set
|
|
228
|
+
// by the per-type aliases; `listStatuses` keeps their own --status choices.
|
|
229
|
+
function buildReviewRequestOperation({
|
|
230
|
+
action,
|
|
231
|
+
target,
|
|
232
|
+
options,
|
|
233
|
+
fixedType = "",
|
|
234
|
+
command = "lumine admin review",
|
|
235
|
+
listStatuses = REVIEW_REQUEST_LIST_STATUSES,
|
|
236
|
+
}) {
|
|
237
|
+
const reason = String(options.adminReason || "").trim();
|
|
238
|
+
if (reason.length > 1000) {
|
|
239
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
240
|
+
}
|
|
241
|
+
if (!action || action === "list") {
|
|
242
|
+
return readOperation(
|
|
243
|
+
"review.list",
|
|
244
|
+
withQuery("/cli/admin/reviews", {
|
|
245
|
+
status: parseChoice(
|
|
246
|
+
options.adminStatus || "pending",
|
|
247
|
+
"--status",
|
|
248
|
+
listStatuses,
|
|
249
|
+
),
|
|
250
|
+
type:
|
|
251
|
+
fixedType ||
|
|
252
|
+
(options.adminType
|
|
253
|
+
? String(options.adminType)
|
|
254
|
+
.split(",")
|
|
255
|
+
.map((value) => parseReviewRequestType(value))
|
|
256
|
+
.join(",")
|
|
257
|
+
: ""),
|
|
258
|
+
cursor: options.adminCursor
|
|
259
|
+
? /^\d+(\.\d\.\d+)?$/.test(String(options.adminCursor))
|
|
260
|
+
? String(options.adminCursor)
|
|
261
|
+
: (() => {
|
|
262
|
+
throw cliValidationError(
|
|
263
|
+
"--cursor must be the value a previous list printed.",
|
|
264
|
+
);
|
|
265
|
+
})()
|
|
266
|
+
: "",
|
|
267
|
+
}),
|
|
268
|
+
{ requiresRun: false, reviewType: fixedType || null },
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const ref = parseReviewRequestRef(target, {
|
|
272
|
+
type: fixedType,
|
|
273
|
+
typeOption: options.adminType,
|
|
274
|
+
});
|
|
275
|
+
if (action === "show" || action === "get") {
|
|
276
|
+
const snapshotDir = String(options.dir || "").trim();
|
|
277
|
+
if (options.dir && !snapshotDir) {
|
|
278
|
+
throw cliValidationError("Pass a new or empty directory with --dir <path>.");
|
|
279
|
+
}
|
|
280
|
+
if (snapshotDir && ref.type !== "rewards") {
|
|
281
|
+
throw cliValidationError("--dir writes a reward request's frozen source.");
|
|
282
|
+
}
|
|
283
|
+
return readOperation(
|
|
284
|
+
"review.show",
|
|
285
|
+
withQuery(`/cli/admin/reviews/${ref.type}/${ref.id}`, {
|
|
286
|
+
// Reward source is listed with sizes only unless --dir asks for it.
|
|
287
|
+
files: snapshotDir ? "1" : "0",
|
|
288
|
+
}),
|
|
289
|
+
{
|
|
290
|
+
requiresRun: false,
|
|
291
|
+
reviewType: ref.type,
|
|
292
|
+
reviewId: ref.id,
|
|
293
|
+
snapshotDir,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (!REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (action === "revoke" && !["rewards", "cardcraft"].includes(ref.type)) {
|
|
301
|
+
throw cliValidationError(
|
|
302
|
+
`${REVIEW_REQUEST_LABELS[ref.type]} approvals are not revoked; change the limit instead${ref.type === "storage-limit" ? " (lumine admin storage grant)" : ""}.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
["rewards", "cardcraft"].includes(ref.type) &&
|
|
307
|
+
action !== "approve" &&
|
|
308
|
+
!reason
|
|
309
|
+
) {
|
|
310
|
+
throw cliValidationError(
|
|
311
|
+
`${command} ${action} <id> needs --reason <text> the creator will read.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const body = { decision: action, reason };
|
|
315
|
+
if (options.adminConfigFile) {
|
|
316
|
+
if (ref.type !== "rewards") {
|
|
317
|
+
throw cliValidationError(
|
|
318
|
+
"--config (earning rules) is only used with XP & Coin reward requests.",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (action !== "approve") {
|
|
322
|
+
throw cliValidationError(
|
|
323
|
+
"--config is only used with approve; rejections and revocations take --reason.",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
// Without --config the app's own proposal (rewards.json + sheet, frozen
|
|
327
|
+
// in the request) is approved as it stands; --config replaces it.
|
|
328
|
+
body.config = readRewardConfigFile(options.adminConfigFile);
|
|
329
|
+
}
|
|
330
|
+
if (options.storageSize) {
|
|
331
|
+
if (ref.type !== "storage-limit") {
|
|
332
|
+
throw cliValidationError(
|
|
333
|
+
"--size is only used with Lumine file storage requests.",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (action !== "approve") {
|
|
337
|
+
throw cliValidationError("--size is only used with approve.");
|
|
338
|
+
}
|
|
339
|
+
body.sizeBytes = parseAdminStorageSize(options.storageSize);
|
|
340
|
+
}
|
|
341
|
+
return writeOperation(
|
|
342
|
+
"review.decide",
|
|
343
|
+
"POST",
|
|
344
|
+
`/cli/admin/reviews/${ref.type}/${ref.id}`,
|
|
345
|
+
body,
|
|
346
|
+
{
|
|
347
|
+
requiresRun: false,
|
|
348
|
+
reviewType: ref.type,
|
|
349
|
+
reviewId: ref.id,
|
|
350
|
+
decision: action,
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// A reviewer counter-proposal for an XP & Coin reward request: the edited copy
|
|
356
|
+
// of the snapshot (from `show --dir`) plus the rules the creator's acceptance
|
|
357
|
+
// publishes.
|
|
358
|
+
function buildRewardProposalOperation(reviewId, options) {
|
|
359
|
+
const reason = String(options.adminReason || "").trim();
|
|
360
|
+
if (reason.length > 1000) {
|
|
361
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
362
|
+
}
|
|
363
|
+
if (!options.adminConfigFile) {
|
|
364
|
+
throw cliValidationError(
|
|
365
|
+
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const files = readRewardProposalDirectory(options.dir);
|
|
369
|
+
return writeOperation(
|
|
370
|
+
"reward-review.propose",
|
|
371
|
+
"POST",
|
|
372
|
+
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
373
|
+
{
|
|
374
|
+
files,
|
|
375
|
+
config: readRewardConfigFile(options.adminConfigFile),
|
|
376
|
+
reason,
|
|
377
|
+
},
|
|
378
|
+
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
144
382
|
const REWARD_PROPOSAL_MAX_FILES = 500;
|
|
145
383
|
const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
146
384
|
const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
|
|
@@ -349,6 +587,26 @@ function readEditorialFile(filePath) {
|
|
|
349
587
|
}
|
|
350
588
|
|
|
351
589
|
export async function adminCommand(options) {
|
|
590
|
+
const [namespace, action] = options.positional || [];
|
|
591
|
+
const composite =
|
|
592
|
+
namespace === "daily-run" && action === "gather"
|
|
593
|
+
? dailyRunGather
|
|
594
|
+
: namespace === "featured" && action === "candidates"
|
|
595
|
+
? featuredCandidates
|
|
596
|
+
: namespace === "report" && action === "serve"
|
|
597
|
+
? reportServe
|
|
598
|
+
: null;
|
|
599
|
+
if (composite) {
|
|
600
|
+
const result = await composite(options);
|
|
601
|
+
if (options.adminOutput) {
|
|
602
|
+
writeFileSync(options.adminOutput, JSON.stringify(result), {
|
|
603
|
+
mode: 0o600,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
console.log(JSON.stringify(result, null, options.json ? 0 : 2));
|
|
607
|
+
if (result && result.ok === false) process.exitCode = 1;
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
352
610
|
const operation = parseAdminOperation(options);
|
|
353
611
|
if (operation.name === "news.validate") {
|
|
354
612
|
const validation = validateNewsEditorial({
|
|
@@ -633,7 +891,11 @@ export async function adminCommand(options) {
|
|
|
633
891
|
if (operation.name === "daily-run.start") {
|
|
634
892
|
assertAdminTodoHandoffResult(result, operation.body.scope);
|
|
635
893
|
}
|
|
636
|
-
if (
|
|
894
|
+
if (
|
|
895
|
+
(operation.name === "reward-review.show" ||
|
|
896
|
+
operation.name === "review.show") &&
|
|
897
|
+
operation.snapshotDir
|
|
898
|
+
) {
|
|
637
899
|
// The snapshot leaves the JSON result and lands on disk, where the agent
|
|
638
900
|
// reads it like any pulled workspace; the result keeps sizes and paths.
|
|
639
901
|
const review = result?.data?.review || {};
|
|
@@ -1597,103 +1859,46 @@ export function parseAdminOperation(options) {
|
|
|
1597
1859
|
}
|
|
1598
1860
|
}
|
|
1599
1861
|
|
|
1862
|
+
if (namespace === "review" || namespace === "reviews") {
|
|
1863
|
+
// Every Build unlock in one queue. Mikey's own decisions: any time, never
|
|
1864
|
+
// part of a delegated Zero/Ciel daily run.
|
|
1865
|
+
if (action === "propose") {
|
|
1866
|
+
const ref = parseReviewRequestRef(target, {
|
|
1867
|
+
typeOption: options.adminType,
|
|
1868
|
+
});
|
|
1869
|
+
if (ref.type !== "rewards") {
|
|
1870
|
+
throw cliValidationError(
|
|
1871
|
+
"Only XP & Coin reward requests take a proposed copy.",
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
return buildRewardProposalOperation(ref.id, options);
|
|
1875
|
+
}
|
|
1876
|
+
const operation = buildReviewRequestOperation({ action, target, options });
|
|
1877
|
+
if (operation) return operation;
|
|
1878
|
+
throw cliValidationError(REVIEW_REQUEST_USAGE);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1600
1881
|
if (
|
|
1601
1882
|
namespace === "reward-review" ||
|
|
1602
1883
|
namespace === "reward-reviews" ||
|
|
1603
1884
|
namespace === "rewards"
|
|
1604
1885
|
) {
|
|
1605
|
-
//
|
|
1606
|
-
// never part of a delegated Zero/Ciel daily run: they can happen any time.
|
|
1607
|
-
const snapshotDir = String(options.dir || "").trim();
|
|
1608
|
-
if (!action || action === "list") {
|
|
1609
|
-
return readOperation(
|
|
1610
|
-
"reward-review.list",
|
|
1611
|
-
withQuery("/cli/admin/reward-reviews", {
|
|
1612
|
-
status: parseChoice(
|
|
1613
|
-
options.adminStatus || "pending",
|
|
1614
|
-
"--status",
|
|
1615
|
-
REWARD_REVIEW_STATUSES,
|
|
1616
|
-
),
|
|
1617
|
-
beforeId: options.adminCursor
|
|
1618
|
-
? parseRequiredInteger(options.adminCursor, "--cursor", 1)
|
|
1619
|
-
: "",
|
|
1620
|
-
}),
|
|
1621
|
-
{ requiresRun: false },
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
1624
|
-
if (action === "show" || action === "get") {
|
|
1625
|
-
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1626
|
-
if (options.dir && !snapshotDir) {
|
|
1627
|
-
throw cliValidationError(
|
|
1628
|
-
"Pass a new or empty directory with --dir <path>.",
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
return readOperation(
|
|
1632
|
-
"reward-review.show",
|
|
1633
|
-
withQuery(`/cli/admin/reward-reviews/${reviewId}`, {
|
|
1634
|
-
// Without --dir the source is listed with sizes only; with --dir the
|
|
1635
|
-
// full snapshot is fetched and written locally for reading.
|
|
1636
|
-
files: snapshotDir ? "1" : "0",
|
|
1637
|
-
}),
|
|
1638
|
-
{ requiresRun: false, reviewId, snapshotDir },
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1886
|
+
// Alias of `lumine admin review ... rewards:<id>`; propose stays here.
|
|
1641
1887
|
if (action === "propose") {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const reason = String(options.adminReason || "").trim();
|
|
1646
|
-
if (reason.length > 1000) {
|
|
1647
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1648
|
-
}
|
|
1649
|
-
if (!options.adminConfigFile) {
|
|
1650
|
-
throw cliValidationError(
|
|
1651
|
-
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
const files = readRewardProposalDirectory(options.dir);
|
|
1655
|
-
return writeOperation(
|
|
1656
|
-
"reward-review.propose",
|
|
1657
|
-
"POST",
|
|
1658
|
-
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
1659
|
-
{
|
|
1660
|
-
files,
|
|
1661
|
-
config: readRewardConfigFile(options.adminConfigFile),
|
|
1662
|
-
reason,
|
|
1663
|
-
},
|
|
1664
|
-
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
1665
|
-
);
|
|
1666
|
-
}
|
|
1667
|
-
if (REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
1668
|
-
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1669
|
-
const reason = String(options.adminReason || "").trim();
|
|
1670
|
-
if (action !== "approve" && !reason) {
|
|
1671
|
-
throw cliValidationError(
|
|
1672
|
-
`lumine admin reward-review ${action} <id> needs --reason <text> the creator will read.`,
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
if (reason.length > 1000) {
|
|
1676
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1677
|
-
}
|
|
1678
|
-
const body = { decision: action, reason };
|
|
1679
|
-
if (action === "approve") {
|
|
1680
|
-
// Without --config the app's own proposal (rewards.json + sheet, frozen
|
|
1681
|
-
// in the request) is approved as it stands; --config replaces it.
|
|
1682
|
-
if (options.adminConfigFile)
|
|
1683
|
-
body.config = readRewardConfigFile(options.adminConfigFile);
|
|
1684
|
-
} else if (options.adminConfigFile) {
|
|
1685
|
-
throw cliValidationError(
|
|
1686
|
-
"--config is only used with approve; rejections and revocations take --reason.",
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
return writeOperation(
|
|
1690
|
-
"reward-review.decide",
|
|
1691
|
-
"POST",
|
|
1692
|
-
`/cli/admin/reward-reviews/${reviewId}`,
|
|
1693
|
-
body,
|
|
1694
|
-
{ requiresRun: false, reviewId, decision: action },
|
|
1888
|
+
return buildRewardProposalOperation(
|
|
1889
|
+
parseRequiredInteger(target, "Reward review ID", 1),
|
|
1890
|
+
options,
|
|
1695
1891
|
);
|
|
1696
1892
|
}
|
|
1893
|
+
const operation = buildReviewRequestOperation({
|
|
1894
|
+
action,
|
|
1895
|
+
target,
|
|
1896
|
+
options,
|
|
1897
|
+
fixedType: "rewards",
|
|
1898
|
+
command: "lumine admin reward-review",
|
|
1899
|
+
listStatuses: REWARD_REVIEW_STATUSES,
|
|
1900
|
+
});
|
|
1901
|
+
if (operation) return operation;
|
|
1697
1902
|
throw cliValidationError(
|
|
1698
1903
|
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | propose <id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1699
1904
|
);
|
|
@@ -1704,54 +1909,75 @@ export function parseAdminOperation(options) {
|
|
|
1704
1909
|
namespace === "cardcraft-reviews" ||
|
|
1705
1910
|
namespace === "cardcraft"
|
|
1706
1911
|
) {
|
|
1707
|
-
//
|
|
1708
|
-
|
|
1912
|
+
// Alias of `lumine admin review ... cardcraft:<id>`.
|
|
1913
|
+
const operation = buildReviewRequestOperation({
|
|
1914
|
+
action,
|
|
1915
|
+
target,
|
|
1916
|
+
options,
|
|
1917
|
+
fixedType: "cardcraft",
|
|
1918
|
+
command: "lumine admin cardcraft-review",
|
|
1919
|
+
listStatuses: REWARD_REVIEW_STATUSES,
|
|
1920
|
+
});
|
|
1921
|
+
if (operation) return operation;
|
|
1922
|
+
throw cliValidationError(
|
|
1923
|
+
"Usage: lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve <id> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (STORAGE_LIMIT_NAMESPACES.has(namespace)) {
|
|
1928
|
+
// Lumine file storage (Twinkle.files) unlocks: per-creator approvals past
|
|
1929
|
+
// the default quota. Mikey's own decision, any time, never in a daily run.
|
|
1930
|
+
const usage =
|
|
1931
|
+
"Usage: lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>].";
|
|
1932
|
+
const reason = String(options.adminReason || "").trim();
|
|
1933
|
+
if (reason.length > 1000) {
|
|
1934
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1935
|
+
}
|
|
1709
1936
|
if (!action || action === "list") {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
? parseRequiredInteger(options.adminCursor, "--cursor", 1)
|
|
1720
|
-
: "",
|
|
1721
|
-
}),
|
|
1722
|
-
{ requiresRun: false },
|
|
1723
|
-
);
|
|
1937
|
+
// Alias of `lumine admin review list --type storage`.
|
|
1938
|
+
return buildReviewRequestOperation({
|
|
1939
|
+
action,
|
|
1940
|
+
target,
|
|
1941
|
+
options,
|
|
1942
|
+
fixedType: "storage-limit",
|
|
1943
|
+
command: "lumine admin storage",
|
|
1944
|
+
listStatuses: ["pending", "approved", "rejected", "all"],
|
|
1945
|
+
});
|
|
1724
1946
|
}
|
|
1725
1947
|
if (action === "show" || action === "get") {
|
|
1726
|
-
const
|
|
1948
|
+
const user = parseStorageTargetUser(target);
|
|
1727
1949
|
return readOperation(
|
|
1728
|
-
"
|
|
1729
|
-
`/cli/admin/
|
|
1730
|
-
{ requiresRun: false
|
|
1950
|
+
"storage.show",
|
|
1951
|
+
`/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
|
|
1952
|
+
{ requiresRun: false },
|
|
1731
1953
|
);
|
|
1732
1954
|
}
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1955
|
+
if (action === "approve" || action === "reject") {
|
|
1956
|
+
// Alias of `lumine admin review approve|reject storage:<id>`.
|
|
1957
|
+
return buildReviewRequestOperation({
|
|
1958
|
+
action,
|
|
1959
|
+
target,
|
|
1960
|
+
options,
|
|
1961
|
+
fixedType: "storage-limit",
|
|
1962
|
+
command: "lumine admin storage",
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
if (action === "grant" || action === "set") {
|
|
1966
|
+
const user = parseStorageTargetUser(target);
|
|
1967
|
+
if (!options.storageSize) {
|
|
1737
1968
|
throw cliValidationError(
|
|
1738
|
-
|
|
1969
|
+
"lumine admin storage grant <user> needs --size <500MB|1GB|2GB|n MB>.",
|
|
1739
1970
|
);
|
|
1740
1971
|
}
|
|
1741
|
-
if (reason.length > 1000) {
|
|
1742
|
-
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1743
|
-
}
|
|
1744
1972
|
return writeOperation(
|
|
1745
|
-
"
|
|
1746
|
-
"
|
|
1747
|
-
`/cli/admin/
|
|
1748
|
-
{
|
|
1749
|
-
{ requiresRun: false
|
|
1973
|
+
"storage.grant",
|
|
1974
|
+
"PUT",
|
|
1975
|
+
`/cli/admin/storage-limits/users/${encodeURIComponent(user)}`,
|
|
1976
|
+
{ sizeBytes: parseAdminStorageSize(options.storageSize), reason },
|
|
1977
|
+
{ requiresRun: false },
|
|
1750
1978
|
);
|
|
1751
1979
|
}
|
|
1752
|
-
throw cliValidationError(
|
|
1753
|
-
"Usage: lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve <id> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1754
|
-
);
|
|
1980
|
+
throw cliValidationError(usage);
|
|
1755
1981
|
}
|
|
1756
1982
|
|
|
1757
1983
|
if (namespace === "chat-reports" || namespace === "chat-report") {
|
|
@@ -4458,6 +4684,190 @@ function printCardCraftReviewResult({ operation, data }) {
|
|
|
4458
4684
|
);
|
|
4459
4685
|
}
|
|
4460
4686
|
|
|
4687
|
+
const STORAGE_LIMIT_NAMESPACES = new Set([
|
|
4688
|
+
"storage",
|
|
4689
|
+
"storage-limit",
|
|
4690
|
+
"storage-limits",
|
|
4691
|
+
"storage-request",
|
|
4692
|
+
"storage-requests",
|
|
4693
|
+
]);
|
|
4694
|
+
|
|
4695
|
+
function parseStorageTargetUser(value) {
|
|
4696
|
+
const user = String(value || "")
|
|
4697
|
+
.trim()
|
|
4698
|
+
.replace(/^@/, "");
|
|
4699
|
+
if (!user || !/^[\w.-]+$/.test(user)) {
|
|
4700
|
+
throw cliValidationError("Pass a user id or username.");
|
|
4701
|
+
}
|
|
4702
|
+
return user;
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
function parseAdminStorageSize(value) {
|
|
4706
|
+
try {
|
|
4707
|
+
return parseStorageSizeBytes(value);
|
|
4708
|
+
} catch (error) {
|
|
4709
|
+
throw cliValidationError(error.message);
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
function formatStorageRequestLine(request) {
|
|
4714
|
+
const approved = request.approvedMaxRuntimeFileStorageBytes
|
|
4715
|
+
? ` → ${formatBytes(request.approvedMaxRuntimeFileStorageBytes)}`
|
|
4716
|
+
: "";
|
|
4717
|
+
return `#${request.requestId} · ${request.status} · ${request.username || `user ${request.userId}`} (${request.userId}) asks ${formatBytes(request.requestedMaxRuntimeFileStorageBytes)}${approved} · using ${formatBytes(request.currentUsageBytes)} at request${request.reason ? ` · "${request.reason}"` : ""}${request.reviewReason ? ` · review: ${request.reviewReason}` : ""}`;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
function printStorageLimitResult({ operation, data, result }) {
|
|
4721
|
+
if (operation.name === "storage.list") {
|
|
4722
|
+
const requests = Array.isArray(data.requests) ? data.requests : [];
|
|
4723
|
+
console.log(
|
|
4724
|
+
`${requests.length} storage request(s) (${data.filter || "pending"}):`,
|
|
4725
|
+
);
|
|
4726
|
+
for (const request of requests)
|
|
4727
|
+
console.log(` ${formatStorageRequestLine(request)}`);
|
|
4728
|
+
if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
if (operation.name === "storage.show") {
|
|
4732
|
+
const storage = data.storage || {};
|
|
4733
|
+
console.log(
|
|
4734
|
+
`${data.user?.username || "user"} (${data.user?.id}): ${formatBytes(storage.runtimeFileStorageBytes)} used of ${formatBytes(storage.maxRuntimeFileStorageBytes)} across ${Number(storage.runtimeFileCount || 0)} file(s) (default ${formatBytes(storage.defaultMaxRuntimeFileStorageBytes)}, cap ${formatBytes(storage.maxApprovableRuntimeFileStorageBytes)}).`,
|
|
4735
|
+
);
|
|
4736
|
+
if (data.override) {
|
|
4737
|
+
console.log(
|
|
4738
|
+
`Approved override: ${formatBytes(data.override.maxRuntimeFileStorageBytes)} by user ${data.override.approvedByUserId}${data.override.reason ? ` · ${data.override.reason}` : ""}`,
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
for (const request of data.recentRequests || [])
|
|
4742
|
+
console.log(` ${formatStorageRequestLine(request)}`);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
const limit = formatBytes(data.maxRuntimeFileStorageBytes);
|
|
4746
|
+
if (operation.name === "storage.decide") {
|
|
4747
|
+
const request = data.request || {};
|
|
4748
|
+
console.log(
|
|
4749
|
+
result?.changed === false
|
|
4750
|
+
? `Storage request #${request.requestId} was already ${request.status}. ${request.username || "The creator"}'s limit is ${limit}.`
|
|
4751
|
+
: `Storage request #${request.requestId} ${request.status}. ${request.username || "The creator"}'s Lumine file storage limit is now ${limit}.`,
|
|
4752
|
+
);
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
if (operation.name === "storage.grant") {
|
|
4756
|
+
console.log(
|
|
4757
|
+
result?.changed === false
|
|
4758
|
+
? `${data.user?.username || "The user"} (${data.user?.id}) already has ${limit} of Lumine file storage.`
|
|
4759
|
+
: `${data.user?.username || "The user"} (${data.user?.id}) now has ${limit} of Lumine file storage.${data.request ? ` Closed pending request #${data.request.requestId} as approved.` : ""}`,
|
|
4760
|
+
);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
function formatReviewAge(seconds) {
|
|
4765
|
+
const at = Number(seconds || 0);
|
|
4766
|
+
if (!at) return "";
|
|
4767
|
+
const minutes = Math.max(0, Math.round((Date.now() / 1000 - at) / 60));
|
|
4768
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4769
|
+
const hours = Math.round(minutes / 60);
|
|
4770
|
+
if (hours < 48) return `${hours}h ago`;
|
|
4771
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
function formatReviewItemLine(item) {
|
|
4775
|
+
const where =
|
|
4776
|
+
item.appTitle || (item.type === "storage-limit" ? "all their Builds" : "");
|
|
4777
|
+
return [
|
|
4778
|
+
`${item.ref} · ${item.status}${item.details?.isLive ? " (live)" : ""}`,
|
|
4779
|
+
REVIEW_REQUEST_LABELS[item.type] || item.type,
|
|
4780
|
+
`${where ? `${where} · ` : ""}by ${item.requesterUsername || "user"} (${item.requesterId})`,
|
|
4781
|
+
item.summary,
|
|
4782
|
+
item.reason ? `"${item.reason}"` : "",
|
|
4783
|
+
item.reviewReason && item.status !== "pending" ? `note: ${item.reviewReason}` : "",
|
|
4784
|
+
formatReviewAge(item.createdAt),
|
|
4785
|
+
]
|
|
4786
|
+
.filter(Boolean)
|
|
4787
|
+
.join(" · ");
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
function printReviewEvents(events) {
|
|
4791
|
+
for (const event of events || [])
|
|
4792
|
+
console.log(
|
|
4793
|
+
` ${new Date(Number(event.createdAt || 0) * 1000).toISOString()} ${event.action} by ${event.actorId}${event.reason ? `: ${event.reason}` : ""}`,
|
|
4794
|
+
);
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
function printReviewRequestResult({ operation, data, result }) {
|
|
4798
|
+
if (operation.name === "review.list") {
|
|
4799
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
4800
|
+
const scope = operation.reviewType
|
|
4801
|
+
? REVIEW_REQUEST_LABELS[operation.reviewType]
|
|
4802
|
+
: "Build";
|
|
4803
|
+
console.log(`${items.length} ${scope} request(s) (${data.filter || "pending"}):`);
|
|
4804
|
+
for (const item of items) console.log(` ${formatReviewItemLine(item)}`);
|
|
4805
|
+
if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
|
|
4806
|
+
if (items.some((item) => item.decisions?.includes("approve")))
|
|
4807
|
+
console.log(
|
|
4808
|
+
"Decide: lumine admin review approve|reject <ref> (storage: --size; rewards: --config; cardcraft/rewards reject needs --reason).",
|
|
4809
|
+
);
|
|
4810
|
+
return;
|
|
4811
|
+
}
|
|
4812
|
+
const type = operation.reviewType;
|
|
4813
|
+
// Rewards and card crafting keep their detailed printers.
|
|
4814
|
+
if (type === "rewards") {
|
|
4815
|
+
printRewardReviewResult({
|
|
4816
|
+
operation: {
|
|
4817
|
+
...operation,
|
|
4818
|
+
name:
|
|
4819
|
+
operation.name === "review.decide"
|
|
4820
|
+
? "reward-review.decide"
|
|
4821
|
+
: "reward-review.show",
|
|
4822
|
+
},
|
|
4823
|
+
data,
|
|
4824
|
+
});
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
if (type === "cardcraft") {
|
|
4828
|
+
printCardCraftReviewResult({
|
|
4829
|
+
operation: {
|
|
4830
|
+
...operation,
|
|
4831
|
+
name:
|
|
4832
|
+
operation.name === "review.decide"
|
|
4833
|
+
? "cardcraft-review.decide"
|
|
4834
|
+
: "cardcraft-review.show",
|
|
4835
|
+
},
|
|
4836
|
+
data,
|
|
4837
|
+
});
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
if (operation.name === "review.decide" && type === "storage-limit") {
|
|
4841
|
+
printStorageLimitResult({
|
|
4842
|
+
operation: { ...operation, name: "storage.decide" },
|
|
4843
|
+
data,
|
|
4844
|
+
result,
|
|
4845
|
+
});
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
if (data.item) console.log(formatReviewItemLine(data.item));
|
|
4849
|
+
if (operation.name === "review.decide") {
|
|
4850
|
+
console.log(
|
|
4851
|
+
result?.changed === false
|
|
4852
|
+
? `Request ${data.item?.ref || ""} was already ${data.item?.status || data.request?.status}.`
|
|
4853
|
+
: `Decision recorded: ${operation.decision}.${type === "project-limit" && operation.decision === "approve" ? " Main and all its branches have the room now." : ""}`,
|
|
4854
|
+
);
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
if (type === "storage-limit" && data.storage) {
|
|
4858
|
+
const storage = data.storage;
|
|
4859
|
+
console.log(
|
|
4860
|
+
`Now: ${formatBytes(storage.runtimeFileStorageBytes)} used of ${formatBytes(storage.maxRuntimeFileStorageBytes)} across ${Number(storage.runtimeFileCount || 0)} file(s) (default ${formatBytes(storage.defaultMaxRuntimeFileStorageBytes)}, cap ${formatBytes(storage.maxApprovableRuntimeFileStorageBytes)}).`,
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
if (type === "project-limit" && data.currentLimits) {
|
|
4864
|
+
console.log(
|
|
4865
|
+
`Now: ${data.currentLimits.maxFilesPerProject} files, ${formatBytes(data.currentLimits.maxProjectBytes)} (Main; every branch inherits it).`,
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
printReviewEvents(data.events);
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4461
4871
|
function printAdminResult({ operation, result }) {
|
|
4462
4872
|
const data = result?.data || {};
|
|
4463
4873
|
if (printChatSafetyResult({ operation, data })) return;
|
|
@@ -4465,6 +4875,10 @@ function printAdminResult({ operation, result }) {
|
|
|
4465
4875
|
printRewardActivity(data);
|
|
4466
4876
|
return;
|
|
4467
4877
|
}
|
|
4878
|
+
if (operation.name.startsWith("review.")) {
|
|
4879
|
+
printReviewRequestResult({ operation, data, result });
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4468
4882
|
if (operation.name.startsWith("reward-review.")) {
|
|
4469
4883
|
printRewardReviewResult({ operation, data });
|
|
4470
4884
|
return;
|
|
@@ -4473,6 +4887,10 @@ function printAdminResult({ operation, result }) {
|
|
|
4473
4887
|
printCardCraftReviewResult({ operation, data });
|
|
4474
4888
|
return;
|
|
4475
4889
|
}
|
|
4890
|
+
if (operation.name.startsWith("storage.")) {
|
|
4891
|
+
printStorageLimitResult({ operation, data, result });
|
|
4892
|
+
return;
|
|
4893
|
+
}
|
|
4476
4894
|
if (operation.name === "runtime.evidence") {
|
|
4477
4895
|
const evidence = data.evidence || {};
|
|
4478
4896
|
console.log(
|