@stage5/lumine 0.2.15 → 0.2.16

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 CHANGED
@@ -10,6 +10,7 @@ npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Priv
10
10
  npx @stage5/lumine@latest rename "My New Build Title"
11
11
  npx @stage5/lumine@latest rename "New Title" --target 123
12
12
  npx @stage5/lumine@latest projects
13
+ npx @stage5/lumine@latest branches 884
13
14
  npx @stage5/lumine@latest explore --sort forks
14
15
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
15
16
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
@@ -35,6 +36,10 @@ pulling the owner's main project creates or reuses your contribution branch and
35
36
  checks out that branch locally. Saves go to your branch, so the project owner
36
37
  can merge or replace main from Twinkle.
37
38
 
39
+ Use `lumine branches <build-url-or-id>` to list the contribution branches you
40
+ can review, including each contributor, branch number, status, and URL. Then use
41
+ `lumine diff <branch-url>` to inspect one branch.
42
+
38
43
  Use `lumine explore` to list public open-source Build apps that can be used as
39
44
  examples or starting points. It supports `--search` and `--sort forks`,
40
45
  `--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
package/lib/api.js CHANGED
@@ -30,6 +30,21 @@ export async function listOpenSourceBuilds({ options, auth }) {
30
30
  return Array.isArray(result.builds) ? result.builds : [];
31
31
  }
32
32
 
33
+ export async function listContributionBranches({
34
+ options,
35
+ auth,
36
+ buildId,
37
+ limit,
38
+ }) {
39
+ const url = new URL(`${options.apiUrl}/build/${buildId}/contributions`);
40
+ url.searchParams.set("limit", String(limit));
41
+ return await requestJson({
42
+ url: url.toString(),
43
+ authToken: auth.token,
44
+ timeoutMs: options.timeoutMs,
45
+ });
46
+ }
47
+
33
48
  export async function loadBuildMetadata({ options, auth, buildId }) {
34
49
  const result = await loadBuildFiles({
35
50
  options,
package/lib/commands.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  loadBuildVersionFiles,
32
32
  loadBuildVersions,
33
33
  loadContributionDiff,
34
+ listContributionBranches,
34
35
  loadLumineCliVersionInfo,
35
36
  loadOpenSourceBuildFiles,
36
37
  maybeCheckForLumineCliUpdate,
@@ -76,6 +77,7 @@ import {
76
77
  collectProjectLimitFindings,
77
78
  findLocalProjectMetadata,
78
79
  isIndexHtmlPath,
80
+ isReadOnlyProjectMetadata,
79
81
  isReadOnlyReferenceMetadata,
80
82
  removeLocalProjectFilesNotIn,
81
83
  stashLocalProjectFilesBeforePull,
@@ -130,6 +132,10 @@ export async function main() {
130
132
  await projects(options);
131
133
  return;
132
134
  }
135
+ if (options.command === "branches") {
136
+ await branches(options);
137
+ return;
138
+ }
133
139
  if (options.command === "explore") {
134
140
  await explore(options);
135
141
  return;
@@ -263,7 +269,8 @@ export async function renameBuild(options) {
263
269
  await saveSelectedBuild({ options, auth, build: updatedBuild });
264
270
  if (
265
271
  localProject?.rootDir &&
266
- Number(localProject.metadata?.buildId || 0) === buildId
272
+ Number(localProject.metadata?.buildId || 0) === buildId &&
273
+ !isReadOnlyProjectMetadata(localProject.metadata)
267
274
  ) {
268
275
  await writeProjectMetadata({
269
276
  dir: localProject.rootDir,
@@ -303,6 +310,33 @@ export async function projects(options) {
303
310
  printBuildList(builds);
304
311
  }
305
312
 
313
+ export async function branches(options) {
314
+ const auth = await resolveAuth(options);
315
+ const requestedBuildId = await resolveRequiredBuildIdOrSelected(
316
+ options,
317
+ auth,
318
+ );
319
+ const requestedBuild = await loadBuildMetadata({
320
+ options,
321
+ auth,
322
+ buildId: requestedBuildId,
323
+ });
324
+ const rootBuildId =
325
+ Number(requestedBuild.contributionRootBuildId || 0) ||
326
+ Number(requestedBuild.id || 0);
327
+ const rootBuild =
328
+ rootBuildId === Number(requestedBuild.id || 0)
329
+ ? requestedBuild
330
+ : await loadBuildMetadata({ options, auth, buildId: rootBuildId });
331
+ const result = await listContributionBranches({
332
+ options,
333
+ auth,
334
+ buildId: rootBuildId,
335
+ limit: options.limit,
336
+ });
337
+ printContributionBranches({ result, rootBuild, options });
338
+ }
339
+
306
340
  export async function explore(options) {
307
341
  const auth = await resolveAuth(options);
308
342
  const builds = await listOpenSourceBuilds({ options, auth });
@@ -1618,6 +1652,34 @@ export function printContributionDiff({ result, build }) {
1618
1652
  }
1619
1653
  }
1620
1654
 
1655
+ export function printContributionBranches({ result, rootBuild, options }) {
1656
+ const contributions = Array.isArray(result?.contributions)
1657
+ ? result.contributions
1658
+ : [];
1659
+ console.log(`Branches for ${formatBuildTitle(rootBuild)}:`);
1660
+ if (!contributions.length) {
1661
+ console.log("No contribution branches.");
1662
+ return;
1663
+ }
1664
+ for (const [index, contribution] of contributions.entries()) {
1665
+ const branchNumber = Number(contribution.contributionBranchNumber || 0);
1666
+ const branchLabel = branchNumber
1667
+ ? `branch ${branchNumber}`
1668
+ : `branch #${contribution.id}`;
1669
+ const title = String(contribution.title || "Untitled branch").trim();
1670
+ const contributor = String(contribution.username || "unknown").trim();
1671
+ const status = String(contribution.contributionStatus || "draft").trim();
1672
+ console.log(
1673
+ `${index + 1}. ${title} - ${contributor} - ${branchLabel} (${status})`,
1674
+ );
1675
+ if (branchNumber) {
1676
+ console.log(
1677
+ ` ${options.siteUrl}/build/${Number(rootBuild.id)}/${branchNumber}`,
1678
+ );
1679
+ }
1680
+ }
1681
+ }
1682
+
1621
1683
  export function printUpdateFromMainResult({ result, build, dir, mergedFiles }) {
1622
1684
  const branchNumber = Number(build?.contributionBranchNumber || 0) || 0;
1623
1685
  const branchLabel = branchNumber
@@ -1981,6 +2043,7 @@ export function printHelp() {
1981
2043
  lumine new [title]
1982
2044
  lumine rename [title] [--target <twinkle-build-url-or-id>]
1983
2045
  lumine projects
2046
+ lumine branches [twinkle-build-url-or-id] [--limit <n>]
1984
2047
  lumine explore [search terms]
1985
2048
  lumine select [twinkle-build-url]
1986
2049
  lumine pull [twinkle-build-url]
@@ -2016,6 +2079,7 @@ Examples:
2016
2079
  npx @stage5/lumine@latest rename "My New Build Title"
2017
2080
  npx @stage5/lumine@latest rename "New Title" --target 123
2018
2081
  npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
2082
+ npx @stage5/lumine@latest branches 884
2019
2083
  npx @stage5/lumine@latest explore --sort forks
2020
2084
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
2021
2085
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
package/lib/constants.js CHANGED
@@ -308,6 +308,7 @@ export const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
308
308
  export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
309
309
  "pull",
310
310
  "check",
311
+ "branches",
311
312
  "diff",
312
313
  "sdk",
313
314
  "select",
@@ -326,6 +327,7 @@ export const COMMANDS = new Set([
326
327
  "new",
327
328
  "rename",
328
329
  "projects",
330
+ "branches",
329
331
  "explore",
330
332
  "select",
331
333
  "pull",
package/lib/workspace.js CHANGED
@@ -779,14 +779,15 @@ export function resolveProjectDirForSave({ options, localProject }) {
779
779
  export function assertLocalProjectCanBeSaved(localProject) {
780
780
  const metadata = localProject?.metadata;
781
781
  if (!metadata) return;
782
- if (metadata.mainCheckout === true) {
782
+ const readOnlyKind = readOnlyProjectMetadataKind(metadata);
783
+ if (readOnlyKind === "main") {
783
784
  const rootBuildId =
784
785
  Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
785
786
  throw new Error(
786
787
  `This is a read-only checkout of main${rootBuildId ? ` for Build ${rootBuildId}` : ""}. Make edits in your branch workspace (\`lumine pull${rootBuildId ? ` ${rootBuildId}` : ""}\`), and run \`lumine update-from-main\` there to bring main's changes into it.`,
787
788
  );
788
789
  }
789
- if (metadata.versionCheckout === true) {
790
+ if (readOnlyKind === "version") {
790
791
  const checkoutBuildId =
791
792
  Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
792
793
  const checkoutVersion = Number(metadata.checkoutVersion || 0) || 0;
@@ -794,7 +795,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
794
795
  `This is a read-only checkout of a previous save${checkoutVersion ? ` (v${checkoutVersion})` : ""}${checkoutBuildId ? ` for Build ${checkoutBuildId}` : ""}. To bring this save back, run \`lumine restore${checkoutVersion ? ` ${checkoutVersion}` : " <n>"}\` from the editable workspace, then \`lumine save\`.`,
795
796
  );
796
797
  }
797
- if (isReadOnlyReferenceMetadata(metadata)) {
798
+ if (readOnlyKind === "reference") {
798
799
  const sourceBuildId =
799
800
  Number(metadata.reference?.sourceBuildId || 0) ||
800
801
  Number(metadata.buildId || 0) ||
@@ -804,7 +805,7 @@ export function assertLocalProjectCanBeSaved(localProject) {
804
805
  `This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
805
806
  );
806
807
  }
807
- if (metadata.build?.canWrite === false) {
808
+ if (readOnlyKind === "server") {
808
809
  throw new Error(
809
810
  "This Lumine checkout is read-only for the current CLI login. Pull or diff it for review; project-owner branch edits must go through merge or replace-main.",
810
811
  );
@@ -819,6 +820,18 @@ export function isReadOnlyReferenceMetadata(metadata) {
819
820
  );
820
821
  }
821
822
 
823
+ export function isReadOnlyProjectMetadata(metadata) {
824
+ return readOnlyProjectMetadataKind(metadata) !== null;
825
+ }
826
+
827
+ export function readOnlyProjectMetadataKind(metadata) {
828
+ if (metadata?.mainCheckout === true) return "main";
829
+ if (metadata?.versionCheckout === true) return "version";
830
+ if (isReadOnlyReferenceMetadata(metadata)) return "reference";
831
+ if (metadata?.build?.canWrite === false) return "server";
832
+ return null;
833
+ }
834
+
822
835
  export function resolveLocalProjectFilePath({ rootDir, projectPath }) {
823
836
  const relativePath = projectPathToRelativePath(projectPath);
824
837
  const root = path.resolve(rootDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {