@stage5/lumine 0.2.17 → 0.2.19

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/lib/commands.js CHANGED
@@ -21,10 +21,12 @@ import {
21
21
  SDK_REFERENCE_FILE,
22
22
  } from "./constants.js";
23
23
  import {
24
+ adoptBuildThumbnailSuggestion,
24
25
  createBuild,
25
26
  fetchAllRuntimeAssets,
26
27
  forkBuild,
27
28
  listBuilds,
29
+ listBuildSuggestions,
28
30
  listOpenSourceBuilds,
29
31
  loadBuildFiles,
30
32
  loadBuildMetadata,
@@ -37,13 +39,19 @@ import {
37
39
  maybeCheckForLumineCliUpdate,
38
40
  mergeContributionIntoMain,
39
41
  mintBuildApiToken,
42
+ notifyBuildOwnerOfContribution,
40
43
  publishBuild,
41
44
  replaceMainWithContribution,
42
45
  resolveBranchBuild,
43
46
  saveProjectFiles,
47
+ suggestBuildThumbnailToOwner,
44
48
  updateBuildMetadata,
45
49
  } from "./api.js";
46
- import { assetsCommand, writeAssetsManifest } from "./assets.js";
50
+ import {
51
+ assetsCommand,
52
+ confirmPrompt,
53
+ writeAssetsManifest,
54
+ } from "./assets.js";
47
55
  import { thumbnailCommand } from "./thumbnail.js";
48
56
  import {
49
57
  assertAuthScope,
@@ -57,6 +65,7 @@ import {
57
65
  import { probeUrl, requestJson } from "./http.js";
58
66
  import { sdkCommand } from "./sdk.js";
59
67
  import { doctorCommand, normalizePreviewUrl } from "./doctor.js";
68
+ import { adminCommand } from "./admin.js";
60
69
  import {
61
70
  defaultMainCheckoutDir,
62
71
  defaultReferenceDir,
@@ -100,10 +109,15 @@ export async function main() {
100
109
  printHelp();
101
110
  return;
102
111
  }
103
- if (options.updateCheck) {
112
+ if (options.updateCheck && !(options.command === "admin" && options.json)) {
104
113
  await maybeCheckForLumineCliUpdate({ options });
105
114
  }
106
115
 
116
+ if (options.command === "admin") {
117
+ await adminCommand(options);
118
+ return;
119
+ }
120
+
107
121
  if (options.command === "workspace") {
108
122
  await workspace(options);
109
123
  return;
@@ -128,6 +142,10 @@ export async function main() {
128
142
  await renameBuild(options);
129
143
  return;
130
144
  }
145
+ if (options.command === "describe") {
146
+ await describeBuild(options);
147
+ return;
148
+ }
131
149
  if (options.command === "projects") {
132
150
  await projects(options);
133
151
  return;
@@ -136,6 +154,14 @@ export async function main() {
136
154
  await branches(options);
137
155
  return;
138
156
  }
157
+ if (options.command === "suggest") {
158
+ await sendSuggestion(options);
159
+ return;
160
+ }
161
+ if (options.command === "suggestions") {
162
+ await suggestions(options);
163
+ return;
164
+ }
139
165
  if (options.command === "explore") {
140
166
  await explore(options);
141
167
  return;
@@ -239,6 +265,36 @@ export async function renameBuild(options) {
239
265
  'Pass a title: `lumine rename "My New Build Title"` or `lumine rename --title "My New Build Title"`.',
240
266
  );
241
267
  }
268
+ const { buildId, updatedBuild } = await updateOwnedBuildDetails({
269
+ options,
270
+ patch: { title },
271
+ permissionError: (id) => `You cannot rename Build #${id}.`,
272
+ failureMessage: "Failed to rename the Build.",
273
+ });
274
+ console.log(`Renamed Build #${buildId} to "${updatedBuild.title}".`);
275
+ }
276
+
277
+ export async function describeBuild(options) {
278
+ const description = await resolveBuildDescriptionUpdate(options);
279
+ const { updatedBuild } = await updateOwnedBuildDetails({
280
+ options,
281
+ patch: { description },
282
+ permissionError: (id) => `You cannot update Build #${id}.`,
283
+ failureMessage: "Failed to update the Build description.",
284
+ });
285
+ console.log(
286
+ description
287
+ ? `Updated description for ${formatBuildTitle(updatedBuild)}.`
288
+ : `Cleared description for ${formatBuildTitle(updatedBuild)}.`,
289
+ );
290
+ }
291
+
292
+ async function updateOwnedBuildDetails({
293
+ options,
294
+ patch,
295
+ permissionError,
296
+ failureMessage,
297
+ }) {
242
298
  const auth = await ensureAuth(options);
243
299
  await assertAuthScope({ options, auth, scope: "build:write" });
244
300
  const localProject = await findLocalProjectMetadata(
@@ -249,21 +305,21 @@ export async function renameBuild(options) {
249
305
  });
250
306
  const currentBuild = await loadBuildMetadata({ options, auth, buildId });
251
307
  if (currentBuild.canWrite === false) {
252
- throw new Error(`You cannot rename Build #${buildId}.`);
308
+ throw new Error(permissionError(buildId));
253
309
  }
254
310
  if (Number(currentBuild.contributionRootBuildId || 0) > 0) {
255
311
  throw new Error(
256
- "Contribution branches use the original Build title and cannot be renamed.",
312
+ "Contribution branches use the original Build details and cannot change them.",
257
313
  );
258
314
  }
259
315
  const result = await updateBuildMetadata({
260
316
  options,
261
317
  auth,
262
318
  buildId,
263
- patch: { title },
319
+ patch,
264
320
  });
265
321
  if (result?.success !== true || !result?.build) {
266
- throw new Error(result?.error || "Failed to rename the Build.");
322
+ throw new Error(result?.error || failureMessage);
267
323
  }
268
324
  const updatedBuild = { ...currentBuild, ...result.build };
269
325
  await saveSelectedBuild({ options, auth, build: updatedBuild });
@@ -282,7 +338,7 @@ export async function renameBuild(options) {
282
338
  filesHash: localProject.metadata.filesHash || null,
283
339
  });
284
340
  }
285
- console.log(`Renamed Build #${buildId} to "${updatedBuild.title}".`);
341
+ return { buildId, updatedBuild };
286
342
  }
287
343
 
288
344
  export async function workspace(options) {
@@ -337,6 +393,262 @@ export async function branches(options) {
337
393
  printContributionBranches({ result, rootBuild, options });
338
394
  }
339
395
 
396
+ export async function sendSuggestion(options) {
397
+ const suggestionType = String(options.suggestionAction || "").trim();
398
+ if (suggestionType !== "branch" && suggestionType !== "thumbnail") {
399
+ throw new Error(
400
+ "Usage: lumine suggest branch [--note <message>] | lumine suggest thumbnail",
401
+ );
402
+ }
403
+ const auth = await resolveAuth(options);
404
+ await assertAuthScope({ options, auth, scope: "build:write" });
405
+ const build = await loadTargetBuildMetadata({ options, auth });
406
+ const { rootBuildId, contributionBuildId } =
407
+ resolveContributionActionBuildIds(build);
408
+
409
+ if (suggestionType === "branch") {
410
+ await notifyBuildOwnerOfContribution({
411
+ options,
412
+ auth,
413
+ rootBuildId,
414
+ contributionBuildId,
415
+ note: options.note,
416
+ });
417
+ console.log(
418
+ `Sent branch #${contributionBuildId} to the owner of Build #${rootBuildId} for review.`,
419
+ );
420
+ return;
421
+ }
422
+
423
+ await suggestBuildThumbnailToOwner({
424
+ options,
425
+ auth,
426
+ rootBuildId,
427
+ contributionBuildId,
428
+ });
429
+ console.log(
430
+ `Suggested branch #${contributionBuildId}'s thumbnail to the owner of Build #${rootBuildId}.`,
431
+ );
432
+ }
433
+
434
+ export async function suggestions(options) {
435
+ const auth = await resolveAuth(options);
436
+ const rootBuildId = await resolveSuggestionRootBuildId(options, auth);
437
+ const action = String(options.suggestionAction || "list");
438
+ const requestedSuggestionId = Number(options.suggestionId || 0);
439
+ if (
440
+ action !== "list" &&
441
+ (!Number.isSafeInteger(requestedSuggestionId) || requestedSuggestionId <= 0)
442
+ ) {
443
+ throw new Error(
444
+ `Pass a suggestion ID: lumine suggestions ${action} <suggestion-id>`,
445
+ );
446
+ }
447
+ const result = await listBuildSuggestions({
448
+ options,
449
+ auth,
450
+ rootBuildId,
451
+ cursor: action === "list" ? options.cursor : 0,
452
+ suggestionId: action === "list" ? 0 : requestedSuggestionId,
453
+ });
454
+ const items = Array.isArray(result?.suggestions) ? result.suggestions : [];
455
+
456
+ if (action === "list") {
457
+ if (options.json) {
458
+ console.log(
459
+ JSON.stringify({
460
+ buildId: rootBuildId,
461
+ suggestions: items,
462
+ hasMore: Boolean(result?.hasMore),
463
+ nextCursor: Number(result?.nextCursor || 0) || null,
464
+ }),
465
+ );
466
+ return;
467
+ }
468
+ printBuildSuggestions({
469
+ rootBuildId,
470
+ suggestions: items,
471
+ hasMore: Boolean(result?.hasMore),
472
+ nextCursor: Number(result?.nextCursor || 0),
473
+ });
474
+ return;
475
+ }
476
+
477
+ const suggestionId = requestedSuggestionId;
478
+ const suggestion = items.find(
479
+ (item) => Number(item?.id || 0) === suggestionId,
480
+ );
481
+ if (!suggestion) {
482
+ throw new Error(
483
+ `Open suggestion #${suggestionId} was not found for Build #${rootBuildId}. Run \`lumine suggestions --build ${rootBuildId}\` to refresh the inbox.`,
484
+ );
485
+ }
486
+ await assertAuthScope({ options, auth, scope: "build:write" });
487
+ const contributionBuildId = Number(suggestion.branchBuildId || 0);
488
+ if (!contributionBuildId) {
489
+ throw new Error(`Suggestion #${suggestionId} has no active branch.`);
490
+ }
491
+
492
+ if (action === "merge") {
493
+ if (suggestion.type !== "branch") {
494
+ throw new Error(`Suggestion #${suggestionId} is not a branch suggestion.`);
495
+ }
496
+ const mergeResult = await mergeContributionIntoMain({
497
+ options,
498
+ auth,
499
+ rootBuildId,
500
+ contributionBuildId,
501
+ });
502
+ printContributionActionResult({
503
+ action: "Merged",
504
+ result: mergeResult,
505
+ rootBuildId,
506
+ contributionBuildId,
507
+ });
508
+ return;
509
+ }
510
+
511
+ if (action === "replace-main") {
512
+ if (suggestion.type !== "branch") {
513
+ throw new Error(`Suggestion #${suggestionId} is not a branch suggestion.`);
514
+ }
515
+ const replaceResult = await replaceMainWithContribution({
516
+ options,
517
+ auth,
518
+ rootBuildId,
519
+ contributionBuildId,
520
+ });
521
+ printContributionActionResult({
522
+ action: "Replaced main with",
523
+ result: replaceResult,
524
+ rootBuildId,
525
+ contributionBuildId,
526
+ });
527
+ return;
528
+ }
529
+
530
+ if (action === "adopt-thumbnail") {
531
+ if (suggestion.type !== "thumbnail") {
532
+ throw new Error(
533
+ `Suggestion #${suggestionId} is not a thumbnail suggestion.`,
534
+ );
535
+ }
536
+ if (suggestion.currentThumbnailUrl && !options.assumeYes) {
537
+ const confirmed = await confirmPrompt(
538
+ `Replace Build #${rootBuildId}'s thumbnail with suggestion #${suggestionId}? [y/N] `,
539
+ );
540
+ if (confirmed === null) {
541
+ console.log("Not a TTY — re-run with --yes to replace the thumbnail.");
542
+ return;
543
+ }
544
+ if (!confirmed) {
545
+ console.log("Aborted. Thumbnail unchanged.");
546
+ return;
547
+ }
548
+ }
549
+ const adoptResult = await adoptBuildThumbnailSuggestion({
550
+ options,
551
+ auth,
552
+ rootBuildId,
553
+ contributionBuildId,
554
+ suggestionMessageId: suggestionId,
555
+ thumbnailUrl: suggestion.suggestedThumbnailUrl,
556
+ });
557
+ const canonicalThumbnailUrl = String(
558
+ adoptResult?.build?.thumbnailUrl || "",
559
+ );
560
+ console.log(
561
+ `Applied thumbnail suggestion #${suggestionId} to Build #${rootBuildId}.`,
562
+ );
563
+ if (canonicalThumbnailUrl) console.log(` ${canonicalThumbnailUrl}`);
564
+ return;
565
+ }
566
+
567
+ throw new Error(
568
+ "Usage: lumine suggestions [build] | lumine suggestions merge|replace-main|adopt-thumbnail <suggestion-id> [--build <id>]",
569
+ );
570
+ }
571
+
572
+ async function resolveSuggestionRootBuildId(options, auth) {
573
+ const requestedBuildId = options.buildIdFlag
574
+ ? resolveRequiredBuildId(options.buildIdFlag)
575
+ : await resolveRequiredBuildIdOrSelected(options, auth);
576
+ const build = await loadBuildMetadata({
577
+ options,
578
+ auth,
579
+ buildId: requestedBuildId,
580
+ });
581
+ return (
582
+ Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0)
583
+ );
584
+ }
585
+
586
+ export function printBuildSuggestions({
587
+ rootBuildId,
588
+ suggestions,
589
+ hasMore = false,
590
+ nextCursor = 0,
591
+ }) {
592
+ console.log(`Open suggestions for Build #${rootBuildId}:`);
593
+ if (!suggestions.length) {
594
+ console.log(
595
+ hasMore
596
+ ? "No open branch or thumbnail suggestions on this page."
597
+ : "No open branch or thumbnail suggestions.",
598
+ );
599
+ if (hasMore && nextCursor > 0) {
600
+ console.log(
601
+ `Next page: lumine suggestions --build ${rootBuildId} --cursor ${nextCursor}`,
602
+ );
603
+ }
604
+ return;
605
+ }
606
+ for (const suggestion of suggestions) {
607
+ const suggestionId = Number(suggestion.id || 0);
608
+ const branchNumber = Number(suggestion.branchNumber || 0);
609
+ const branchLabel = branchNumber
610
+ ? `branch ${branchNumber}`
611
+ : `branch #${suggestion.branchBuildId}`;
612
+ const contributor = suggestion.contributorUsername || "a contributor";
613
+ const createdAt = formatVersionTimestamp(suggestion.createdAt);
614
+ if (suggestion.type === "branch") {
615
+ console.log(
616
+ `[#${suggestionId}] ${contributor} submitted ${branchLabel} (${createdAt})`,
617
+ );
618
+ if (suggestion.note) console.log(` ${suggestion.note}`);
619
+ const summary = suggestion.diffSummary || {};
620
+ console.log(
621
+ ` files: ${Number(summary.total || suggestion.changedFiles?.length || 0)} changed`,
622
+ );
623
+ if (suggestion.hasNewerWorkSinceSubmission) {
624
+ console.log(" note: the branch changed after this suggestion");
625
+ }
626
+ console.log(
627
+ ` merge: lumine suggestions merge ${suggestionId} --build ${rootBuildId}`,
628
+ );
629
+ console.log(
630
+ ` replace: lumine suggestions replace-main ${suggestionId} --build ${rootBuildId}`,
631
+ );
632
+ continue;
633
+ }
634
+ console.log(
635
+ `[#${suggestionId}] ${contributor} suggested a thumbnail from ${branchLabel} (${createdAt})`,
636
+ );
637
+ console.log(` ${suggestion.suggestedThumbnailUrl}`);
638
+ if (suggestion.hasNewerThumbnailSinceSuggestion) {
639
+ console.log(" note: the branch has a newer thumbnail now");
640
+ }
641
+ console.log(
642
+ ` apply: lumine suggestions adopt-thumbnail ${suggestionId} --build ${rootBuildId}`,
643
+ );
644
+ }
645
+ if (hasMore && nextCursor > 0) {
646
+ console.log(
647
+ `Next page: lumine suggestions --build ${rootBuildId} --cursor ${nextCursor}`,
648
+ );
649
+ }
650
+ }
651
+
340
652
  export async function explore(options) {
341
653
  const auth = await resolveAuth(options);
342
654
  const builds = await listOpenSourceBuilds({ options, auth });
@@ -1557,9 +1869,15 @@ export function printPullResult(result) {
1557
1869
  );
1558
1870
  console.log('Save after edits: lumine save --summary "Describe the change"');
1559
1871
  if (isContributionBranch(build) && build.canPublish === false) {
1560
- console.log("The project owner can merge or replace main from Twinkle.");
1872
+ console.log(
1873
+ 'Notify the owner when ready: lumine suggest branch "Ready for review"',
1874
+ );
1875
+ console.log(
1876
+ "Offer this branch's thumbnail: lumine suggest thumbnail",
1877
+ );
1561
1878
  } else {
1562
1879
  console.log("Run `lumine check` or `lumine launch --save` when ready.");
1880
+ console.log("Review team nudges: lumine suggestions");
1563
1881
  }
1564
1882
  }
1565
1883
 
@@ -1616,12 +1934,16 @@ export function printSaveResult({ result, build, dir, files }) {
1616
1934
  console.log(`Release status: ${releaseState}`);
1617
1935
  if (isContributionBranch(build) && build.canPublish === false) {
1618
1936
  console.log(
1619
- "Next: the project owner can merge or replace main from Twinkle.",
1937
+ 'Next: notify the project owner with `lumine suggest branch "Ready for review"`.',
1938
+ );
1939
+ console.log(
1940
+ "To offer this branch's thumbnail, run `lumine suggest thumbnail`.",
1620
1941
  );
1621
1942
  } else {
1622
1943
  console.log(
1623
1944
  "Next: run `lumine launch` to publish, or `lumine save --publish` next time.",
1624
1945
  );
1946
+ console.log("Team suggestions: run `lumine suggestions` to review them.");
1625
1947
  }
1626
1948
  }
1627
1949
 
@@ -1772,6 +2094,8 @@ export function parseArgs(args) {
1772
2094
  "json",
1773
2095
  "keepAssets",
1774
2096
  "noBrowser",
2097
+ "includeComments",
2098
+ "anyoneCanReward",
1775
2099
  ]);
1776
2100
 
1777
2101
  for (let i = 0; i < rest.length; i += 1) {
@@ -1791,14 +2115,76 @@ export function parseArgs(args) {
1791
2115
  } else if (booleanFlags.has(camelKey)) {
1792
2116
  raw[camelKey] = true;
1793
2117
  } else {
1794
- raw[camelKey] = rest[i + 1] ?? "";
2118
+ const nextValue = rest[i + 1];
2119
+ if (
2120
+ nextValue === undefined ||
2121
+ nextValue === "-h" ||
2122
+ nextValue.startsWith("--")
2123
+ ) {
2124
+ throw new Error(`Missing value for --${key}.`);
2125
+ }
2126
+ raw[camelKey] = nextValue;
1795
2127
  i += 1;
1796
2128
  }
1797
2129
  }
1798
2130
 
2131
+ const suggestionInboxActions = new Set([
2132
+ "list",
2133
+ "merge",
2134
+ "replace-main",
2135
+ "adopt-thumbnail",
2136
+ ]);
2137
+ const suggestionAction =
2138
+ command === "suggest"
2139
+ ? String(positional[0] || "")
2140
+ : command === "suggestions" &&
2141
+ suggestionInboxActions.has(String(positional[0] || ""))
2142
+ ? String(positional[0])
2143
+ : "list";
2144
+ const suggestionListTarget =
2145
+ command === "suggestions" && suggestionAction === "list"
2146
+ ? String(
2147
+ positional[0] === "list" ? positional[1] || "" : positional[0] || "",
2148
+ )
2149
+ : "";
2150
+
1799
2151
  return {
1800
2152
  command,
1801
2153
  positional,
2154
+ suggestionAction,
2155
+ suggestionId:
2156
+ command === "suggestions" && suggestionAction !== "list"
2157
+ ? String(positional[1] || "")
2158
+ : "",
2159
+ note:
2160
+ String(
2161
+ raw.note ||
2162
+ (command === "suggest" && suggestionAction === "branch"
2163
+ ? positional.slice(1).join(" ")
2164
+ : ""),
2165
+ ).trim() || "",
2166
+ cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
2167
+ adminCursor: raw.cursor ? String(raw.cursor) : "",
2168
+ adminAfter: raw.after ? String(raw.after) : "",
2169
+ adminEffort: raw.effort ? String(raw.effort) : "",
2170
+ adminLevel: raw.level ? String(raw.level) : "",
2171
+ adminIds: raw.subjectIds
2172
+ ? String(raw.subjectIds)
2173
+ : raw.ids
2174
+ ? String(raw.ids)
2175
+ : "",
2176
+ adminType: raw.type ? String(raw.type) : "",
2177
+ adminKind: raw.kind ? String(raw.kind) : "",
2178
+ adminIdentity: raw.identity ? String(raw.identity) : "",
2179
+ commentMode: raw.commentMode ? String(raw.commentMode) : "",
2180
+ runKey: raw.runKey ? String(raw.runKey) : "",
2181
+ idempotencyKey: raw.idempotencyKey ? String(raw.idempotencyKey) : "",
2182
+ draftId: raw.draftId ? String(raw.draftId) : "",
2183
+ twinkles: raw.twinkles ? String(raw.twinkles) : "",
2184
+ adminReason: raw.reason ? String(raw.reason) : "",
2185
+ rewardTwinkles: raw.rewardTwinkles ? String(raw.rewardTwinkles) : "",
2186
+ includeComments: parseBoolean(raw.includeComments, false),
2187
+ anyoneCanReward: parseBoolean(raw.anyoneCanReward, false),
1802
2188
  repeat: Math.min(Math.max(Math.floor(Number(raw.repeat) || 1), 1), 20),
1803
2189
  allowWrite: parseBoolean(raw.allowWrite, false),
1804
2190
  assumeYes: parseBoolean(raw.yes, false),
@@ -1815,7 +2201,13 @@ export function parseArgs(args) {
1815
2201
  target:
1816
2202
  raw.url ||
1817
2203
  raw.target ||
1818
- (command === "rename" ? "" : positional[0] || ""),
2204
+ (command === "rename" ||
2205
+ command === "describe" ||
2206
+ command === "suggest"
2207
+ ? ""
2208
+ : command === "suggestions"
2209
+ ? suggestionListTarget
2210
+ : positional[0] || ""),
1819
2211
  title:
1820
2212
  String(
1821
2213
  raw.title ||
@@ -1825,11 +2217,12 @@ export function parseArgs(args) {
1825
2217
  ).trim() || "",
1826
2218
  description: Object.prototype.hasOwnProperty.call(raw, "description")
1827
2219
  ? String(raw.description || "").trim()
1828
- : null,
1829
- descriptionProvided: Object.prototype.hasOwnProperty.call(
1830
- raw,
1831
- "description",
1832
- ),
2220
+ : command === "describe" && positional.length > 0
2221
+ ? positional.join(" ").trim()
2222
+ : null,
2223
+ descriptionProvided:
2224
+ Object.prototype.hasOwnProperty.call(raw, "description") ||
2225
+ (command === "describe" && positional.length > 0),
1833
2226
  noDescription: parseBoolean(raw.noDescription, false),
1834
2227
  searchQuery:
1835
2228
  String(
@@ -2009,6 +2402,32 @@ export async function resolveNewBuildDescription(options) {
2009
2402
  }
2010
2403
  }
2011
2404
 
2405
+ export async function resolveBuildDescriptionUpdate(options) {
2406
+ if (options.noDescription && options.descriptionProvided) {
2407
+ throw new Error(
2408
+ "Pass either a description or `--no-description`, not both.",
2409
+ );
2410
+ }
2411
+ if (options.noDescription) return null;
2412
+ if (options.descriptionProvided) {
2413
+ return String(options.description || "").trim() || null;
2414
+ }
2415
+ if (!input.isTTY || !output.isTTY) {
2416
+ throw new Error(
2417
+ 'Pass a description: `lumine describe "What this app does"`, or clear it with `lumine describe --no-description`.',
2418
+ );
2419
+ }
2420
+ const rl = readline.createInterface({ input, output });
2421
+ try {
2422
+ const answer = await rl.question(
2423
+ "Build description (leave blank to clear): ",
2424
+ );
2425
+ return String(answer || "").trim() || null;
2426
+ } finally {
2427
+ rl.close();
2428
+ }
2429
+ }
2430
+
2012
2431
  export async function resolveBuildReferenceBuildId({
2013
2432
  options,
2014
2433
  auth,
@@ -2042,8 +2461,15 @@ export function printHelp() {
2042
2461
  lumine logout
2043
2462
  lumine new [title]
2044
2463
  lumine rename [title] [--target <twinkle-build-url-or-id>]
2464
+ lumine describe [description] [--target <twinkle-build-url-or-id>]
2045
2465
  lumine projects
2046
2466
  lumine branches [twinkle-build-url-or-id] [--limit <n>]
2467
+ lumine suggest branch [message] [--target <twinkle-branch-url>]
2468
+ lumine suggest thumbnail [--target <twinkle-branch-url>]
2469
+ lumine suggestions [twinkle-build-url-or-id]
2470
+ lumine suggestions merge <suggestion-id> [--build <id>]
2471
+ lumine suggestions replace-main <suggestion-id> [--build <id>]
2472
+ lumine suggestions adopt-thumbnail <suggestion-id> [--build <id>] [--yes]
2047
2473
  lumine explore [search terms]
2048
2474
  lumine select [twinkle-build-url]
2049
2475
  lumine pull [twinkle-build-url]
@@ -2071,6 +2497,21 @@ export function printHelp() {
2071
2497
  lumine thumbnail capture [--out <file>]
2072
2498
  lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
2073
2499
  lumine doctor runtime-assets
2500
+ lumine admin identity list|status|use <zero|ciel|auto> [--json]
2501
+ lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2502
+ lumine admin daily-run status|complete|fail [--reason <text>] [--json]
2503
+ lumine admin recommendations list [--cursor <cursor>] [--json]
2504
+ lumine admin subjects candidates [--after <date>] [--effort unassigned] [--cursor <cursor>] [--json]
2505
+ lumine admin subject get|comments|reveal <subject-url-or-id> [--cursor <cursor>] [--json]
2506
+ lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
2507
+ lumine admin subject creator set-made-by-poster <subject-id> [--json]
2508
+ lumine admin subject feature|unfeature <subject-id> [--json]
2509
+ lumine admin featured list|reorder --subject-ids <id,id,...> [--json]
2510
+ lumine admin post get|comments <target> [--type subject|comment|aiStory|dailyReflection] [--cursor <cursor>] [--json]
2511
+ lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
2512
+ lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
2513
+ lumine admin comment draft <subject-id> [--identity zero|ciel|auto] [--json]
2514
+ lumine admin comment post --draft-id <id> [--json]
2074
2515
 
2075
2516
  Examples:
2076
2517
  npx @stage5/lumine@latest
@@ -2078,8 +2519,15 @@ Examples:
2078
2519
  npx @stage5/lumine@latest new "Daily Reflection App"
2079
2520
  npx @stage5/lumine@latest rename "My New Build Title"
2080
2521
  npx @stage5/lumine@latest rename "New Title" --target 123
2522
+ npx @stage5/lumine@latest describe "A welcoming place to build together"
2523
+ npx @stage5/lumine@latest describe --no-description --target 123
2081
2524
  npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
2082
2525
  npx @stage5/lumine@latest branches 884
2526
+ npx @stage5/lumine@latest suggest branch "Ready for review"
2527
+ npx @stage5/lumine@latest suggest thumbnail
2528
+ npx @stage5/lumine@latest suggestions 884
2529
+ npx @stage5/lumine@latest suggestions merge 12345 --build 884
2530
+ npx @stage5/lumine@latest suggestions adopt-thumbnail 12346 --build 884 --yes
2083
2531
  npx @stage5/lumine@latest explore --sort forks
2084
2532
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
2085
2533
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
@@ -2105,6 +2553,11 @@ Examples:
2105
2553
  npx @stage5/lumine@latest thumbnail capture --out capture.png
2106
2554
  npx @stage5/lumine@latest thumbnail generate --model gpt-image-2 --yes
2107
2555
  npx @stage5/lumine@latest doctor runtime-assets --build 917 --json
2556
+ npx @stage5/lumine@latest admin daily-run start --identity auto --comment-mode off --json
2557
+ npx @stage5/lumine@latest admin recommendations list --json
2558
+ npx @stage5/lumine@latest admin subjects candidates --effort unassigned --json
2559
+ npx @stage5/lumine@latest admin subject get 123 --include-comments --json
2560
+ npx @stage5/lumine@latest admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
2108
2561
 
2109
2562
  Options:
2110
2563
  --api-url <url> Twinkle API origin
@@ -2113,13 +2566,30 @@ Options:
2113
2566
  --auth-file <path> Saved login path
2114
2567
  --auth-token <token> Override saved login
2115
2568
  --dir <path> Directory for pulled project files
2116
- --target <build> Explicit Build URL or ID for rename (positionals are title-only)
2569
+ --target <build> Explicit Build URL or ID for rename/describe
2117
2570
  --main With pull/versions/restore: target the team project's main
2118
2571
  --version <n> With pull: read-only checkout of previous save v<n>
2119
2572
  --title <text> Build title for new/rename
2120
- --description <text> Optional New Build description
2121
- --no-description Skip the New Build description prompt
2573
+ --description <text> Build description for new/describe
2574
+ --no-description Skip New description or clear with describe
2122
2575
  --summary <text> Save summary
2576
+ --note <text> Message attached to a branch suggestion
2577
+ --cursor <id> Continue an owner suggestion inbox listing
2578
+ --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2579
+ --effort unassigned Admin subjects: show only unassigned effort
2580
+ --identity <mode> Admin identity: zero, ciel, or auto
2581
+ --comment-mode <mode> Admin run comments: off, draft, or post
2582
+ --run-key <key> Idempotency key for an admin daily run
2583
+ --idempotency-key <k> Stable retry key for one admin mutation
2584
+ --level <1|2|3> Admin subject effort level
2585
+ --subject-ids <ids> Complete ordered Featured subject IDs
2586
+ --type <type> Admin target: subject, comment, aiStory, or dailyReflection
2587
+ --kind recommend Admin recommendation queue kind
2588
+ --anyone-can-reward Enable canonical reward eligibility
2589
+ --reward-twinkles 3 Pair a recommendation with exactly 3 Twinkles
2590
+ --twinkles 3 Give exactly 3 Twinkles through the normal economy
2591
+ --draft-id <id> Canonical delegated comment draft ID
2592
+ --reason <text> Reason when marking an admin run failed
2123
2593
  --force Overwrite server files even if this workspace is stale or missing filesHash
2124
2594
  --search <text> Search public open-source Builds
2125
2595
  --sort <sort> Sort open-source Builds: forks, popular, recent
@@ -2128,12 +2598,12 @@ Options:
2128
2598
  --limit <number> Number of projects to show
2129
2599
  --no-update-check Skip the npm latest-version check
2130
2600
  --no-open Print the approval URL without opening a browser
2131
- --build <id> Build id for sdk/assets/thumbnail calls outside a workspace
2601
+ --build <id> Build id for sdk/assets/thumbnail/suggestion calls outside a workspace
2132
2602
  --repeat <n> Repeat an sdk call (1-20) and print latency stats
2133
2603
  --allow-write Permit sdk methods that mutate app data
2134
2604
  --path <api/...> Call an sdk endpoint not in the curated list
2135
2605
  --scopes <a,b> Override requested build API token scopes
2136
- --json Print machine-readable output for doctor commands
2606
+ --json Print machine-readable output where supported
2137
2607
  --keep-assets Keep doctor probe assets instead of deleting them
2138
2608
  --no-browser Skip doctor browser probes
2139
2609
  --model <model> Image model for generate: gpt-image-2 or nano-banana (required, no default)