@stage5/lumine 0.1.6 → 0.1.8

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
@@ -5,6 +5,8 @@ Launch Twinkle Lumine builds from any terminal.
5
5
  ```bash
6
6
  npx @stage5/lumine@latest
7
7
  npx @stage5/lumine@latest login
8
+ npx @stage5/lumine@latest new "Daily Reflection App"
9
+ npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
8
10
  npx @stage5/lumine@latest projects
9
11
  npx @stage5/lumine@latest explore --sort forks
10
12
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
@@ -19,6 +21,13 @@ Run `lumine` with no subcommand for the easiest flow: sign in when needed,
19
21
  choose one of your owned or team projects, and pull the saved project files into
20
22
  a local folder.
21
23
 
24
+ Use `lumine new` to create a new Twinkle Build with the same website create
25
+ route used by Build Studio. The CLI asks for a title when one is not passed and
26
+ asks for an optional description. It creates the Build and pulls an editable
27
+ local workspace, but it does not auto-start a Lumine greeting or prompt run, so
28
+ creating a project from the CLI does not spend AI battery. Add project files
29
+ locally, including `/index.html`, then run `lumine save`.
30
+
22
31
  For team projects, Lumine mirrors the website workspace flow: choosing or
23
32
  pulling the owner's main project creates or reuses your contribution branch and
24
33
  checks out that branch locally. Saves go to your branch, so the project owner
package/bin/lumine.js CHANGED
@@ -119,12 +119,16 @@ const COMMANDS = new Set([
119
119
  "login",
120
120
  "logout",
121
121
  "whoami",
122
+ "new",
122
123
  "projects",
123
124
  "explore",
124
125
  "select",
125
126
  "pull",
126
127
  "reference",
127
128
  "fork",
129
+ "diff",
130
+ "merge",
131
+ "replace-main",
128
132
  "save",
129
133
  "push",
130
134
  "check",
@@ -164,6 +168,10 @@ async function main() {
164
168
  await whoami(options);
165
169
  return;
166
170
  }
171
+ if (options.command === "new") {
172
+ await newBuild(options);
173
+ return;
174
+ }
167
175
  if (options.command === "projects") {
168
176
  await projects(options);
169
177
  return;
@@ -188,6 +196,18 @@ async function main() {
188
196
  await fork(options);
189
197
  return;
190
198
  }
199
+ if (options.command === "diff") {
200
+ await diff(options);
201
+ return;
202
+ }
203
+ if (options.command === "merge") {
204
+ await mergeBranch(options);
205
+ return;
206
+ }
207
+ if (options.command === "replace-main") {
208
+ await replaceMainWithBranch(options);
209
+ return;
210
+ }
191
211
  if (options.command === "save" || options.command === "push") {
192
212
  await save(options);
193
213
  return;
@@ -299,14 +319,30 @@ async function whoami(options) {
299
319
  );
300
320
  }
301
321
 
322
+ async function newBuild(options) {
323
+ const title = await resolveNewBuildTitle(options);
324
+ const description = await resolveNewBuildDescription(options);
325
+ const auth = await ensureAuth(options);
326
+ await assertAuthScope({ options, auth, scope: "build:write" });
327
+ const createResult = await createBuild({
328
+ options,
329
+ auth,
330
+ title,
331
+ description,
332
+ });
333
+ const buildId = Number(createResult.build?.id || 0);
334
+ if (!buildId) {
335
+ throw new Error("Twinkle did not return a created Build.");
336
+ }
337
+ const result = await pullBuildFiles({ options, auth, buildId });
338
+ await saveSelectedBuild({ options, auth, build: result.build });
339
+ printNewBuildResult({ createResult, pullResult: result });
340
+ }
341
+
302
342
  async function workspace(options) {
303
343
  const auth = await ensureAuth(options);
304
344
  const selectedBuild = options.target
305
- ? await loadBuildMetadata({
306
- options,
307
- auth,
308
- buildId: resolveRequiredBuildId(options.target),
309
- })
345
+ ? await loadTargetBuildMetadata({ options, auth })
310
346
  : await chooseProject({
311
347
  builds: await listBuilds({ options, auth }),
312
348
  });
@@ -337,11 +373,7 @@ async function explore(options) {
337
373
  async function selectProject(options) {
338
374
  const auth = await resolveAuth(options);
339
375
  const selectedBuild = options.target
340
- ? await loadBuildMetadata({
341
- options,
342
- auth,
343
- buildId: resolveRequiredBuildId(options.target),
344
- })
376
+ ? await loadTargetBuildMetadata({ options, auth })
345
377
  : await chooseProject({
346
378
  builds: await listBuilds({ options, auth }),
347
379
  });
@@ -376,7 +408,7 @@ async function pull(options) {
376
408
 
377
409
  async function reference(options) {
378
410
  const auth = await resolveAuth(options);
379
- const buildId = resolveRequiredBuildId(options.target);
411
+ const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
380
412
  const result = await pullReferenceFiles({ options, auth, buildId });
381
413
  printReferenceResult(result);
382
414
  }
@@ -384,7 +416,7 @@ async function reference(options) {
384
416
  async function fork(options) {
385
417
  const auth = await resolveAuth(options);
386
418
  await assertAuthScope({ options, auth, scope: "build:write" });
387
- const buildId = resolveRequiredBuildId(options.target);
419
+ const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
388
420
  const forkResult = await forkBuild({ options, auth, buildId });
389
421
  const forkedBuildId = Number(forkResult.build?.id || 0);
390
422
  if (!forkedBuildId) {
@@ -399,6 +431,60 @@ async function fork(options) {
399
431
  printForkResult({ forkResult, pullResult: result });
400
432
  }
401
433
 
434
+ async function diff(options) {
435
+ const auth = await resolveAuth(options);
436
+ const build = await loadTargetBuildMetadata({ options, auth });
437
+ const { rootBuildId, contributionBuildId } =
438
+ resolveContributionActionBuildIds(build);
439
+ const result = await loadContributionDiff({
440
+ options,
441
+ auth,
442
+ rootBuildId,
443
+ contributionBuildId,
444
+ });
445
+ printContributionDiff({ result, build });
446
+ }
447
+
448
+ async function mergeBranch(options) {
449
+ const auth = await resolveAuth(options);
450
+ await assertAuthScope({ options, auth, scope: "build:write" });
451
+ const build = await loadTargetBuildMetadata({ options, auth });
452
+ const { rootBuildId, contributionBuildId } =
453
+ resolveContributionActionBuildIds(build);
454
+ const result = await mergeContributionIntoMain({
455
+ options,
456
+ auth,
457
+ rootBuildId,
458
+ contributionBuildId,
459
+ });
460
+ printContributionActionResult({
461
+ action: "Merged",
462
+ result,
463
+ rootBuildId,
464
+ contributionBuildId,
465
+ });
466
+ }
467
+
468
+ async function replaceMainWithBranch(options) {
469
+ const auth = await resolveAuth(options);
470
+ await assertAuthScope({ options, auth, scope: "build:write" });
471
+ const build = await loadTargetBuildMetadata({ options, auth });
472
+ const { rootBuildId, contributionBuildId } =
473
+ resolveContributionActionBuildIds(build);
474
+ const result = await replaceMainWithContribution({
475
+ options,
476
+ auth,
477
+ rootBuildId,
478
+ contributionBuildId,
479
+ });
480
+ printContributionActionResult({
481
+ action: "Replaced main with",
482
+ result,
483
+ rootBuildId,
484
+ contributionBuildId,
485
+ });
486
+ }
487
+
402
488
  async function save(options) {
403
489
  const auth = await resolveAuth(options);
404
490
  await assertAuthScope({ options, auth, scope: "build:write" });
@@ -415,6 +501,11 @@ async function save(options) {
415
501
  buildId,
416
502
  localProject,
417
503
  });
504
+ if (build?.canWrite === false) {
505
+ throw new Error(
506
+ "This checkout is read-only for the current CLI login. Pull or diff it for review, then merge or replace main from the project owner workflow.",
507
+ );
508
+ }
418
509
  buildId = Number(build?.id || buildId);
419
510
  const dir = resolveProjectDirForSave({ options, localProject });
420
511
  const files = await collectProjectFiles(dir);
@@ -566,6 +657,11 @@ async function publishBuild({ options, buildId, auth }) {
566
657
  function printCheck(result) {
567
658
  const checks = result.checks || {};
568
659
  console.log(`Launch check: ${result.ok ? "ok" : "fail"}`);
660
+ if (checks.canonicalBuild) {
661
+ console.log(
662
+ `- canonical build: ${checks.canonicalBuild.ok ? "ok" : "fail"}`,
663
+ );
664
+ }
569
665
  console.log(
570
666
  `- project files: ${checks.projectFiles?.ok ? "ok" : "fail"} ` +
571
667
  `files=${checks.projectFiles?.fileCount ?? 0}`,
@@ -645,6 +741,11 @@ async function loadBuildMetadata({ options, auth, buildId }) {
645
741
  return result.build;
646
742
  }
647
743
 
744
+ async function loadTargetBuildMetadata({ options, auth }) {
745
+ const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
746
+ return await loadBuildMetadata({ options, auth, buildId });
747
+ }
748
+
648
749
  async function loadOpenSourceBuildFiles({
649
750
  options,
650
751
  auth,
@@ -670,6 +771,24 @@ async function loadBuildFiles({ options, auth, buildId, includeContent }) {
670
771
  });
671
772
  }
672
773
 
774
+ async function resolveBranchBuild({
775
+ options,
776
+ auth,
777
+ rootBuildId,
778
+ branchNumber,
779
+ }) {
780
+ const result = await requestJson({
781
+ url: `${options.apiUrl}/cli/build/${rootBuildId}/branches/${branchNumber}`,
782
+ authToken: auth.token,
783
+ timeoutMs: options.timeoutMs,
784
+ });
785
+ const branchBuildId = Number(result.build?.id || 0);
786
+ if (!branchBuildId) {
787
+ throw new Error(`Branch ${branchNumber} for Build ${rootBuildId} could not be loaded.`);
788
+ }
789
+ return result.build;
790
+ }
791
+
673
792
  async function forkBuild({ options, auth, buildId }) {
674
793
  return await requestJson({
675
794
  method: "POST",
@@ -680,6 +799,19 @@ async function forkBuild({ options, auth, buildId }) {
680
799
  });
681
800
  }
682
801
 
802
+ async function createBuild({ options, auth, title, description }) {
803
+ return await requestJson({
804
+ method: "POST",
805
+ url: `${options.apiUrl}/build/create`,
806
+ authToken: auth.token,
807
+ body: {
808
+ title,
809
+ ...(description ? { description } : {}),
810
+ },
811
+ timeoutMs: options.timeoutMs,
812
+ });
813
+ }
814
+
683
815
  async function saveProjectFiles({ options, auth, buildId, files, summary }) {
684
816
  return await requestJson({
685
817
  method: "PUT",
@@ -694,6 +826,49 @@ async function saveProjectFiles({ options, auth, buildId, files, summary }) {
694
826
  });
695
827
  }
696
828
 
829
+ async function loadContributionDiff({
830
+ options,
831
+ auth,
832
+ rootBuildId,
833
+ contributionBuildId,
834
+ }) {
835
+ return await requestJson({
836
+ url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}`,
837
+ authToken: auth.token,
838
+ timeoutMs: options.timeoutMs,
839
+ });
840
+ }
841
+
842
+ async function mergeContributionIntoMain({
843
+ options,
844
+ auth,
845
+ rootBuildId,
846
+ contributionBuildId,
847
+ }) {
848
+ return await requestJson({
849
+ method: "POST",
850
+ url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/merge`,
851
+ authToken: auth.token,
852
+ body: {},
853
+ timeoutMs: options.timeoutMs,
854
+ });
855
+ }
856
+
857
+ async function replaceMainWithContribution({
858
+ options,
859
+ auth,
860
+ rootBuildId,
861
+ contributionBuildId,
862
+ }) {
863
+ return await requestJson({
864
+ method: "POST",
865
+ url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/replace-main`,
866
+ authToken: auth.token,
867
+ body: {},
868
+ timeoutMs: options.timeoutMs,
869
+ });
870
+ }
871
+
697
872
  async function resolveBuildForSave({ options, auth, buildId, localProject }) {
698
873
  const localBuild = localProject?.metadata?.build;
699
874
  const localBuildId =
@@ -1226,6 +1401,11 @@ function assertLocalProjectCanBeSaved(localProject) {
1226
1401
  `This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
1227
1402
  );
1228
1403
  }
1404
+ if (metadata.build?.canWrite === false) {
1405
+ throw new Error(
1406
+ "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.",
1407
+ );
1408
+ }
1229
1409
  }
1230
1410
 
1231
1411
  function isReadOnlyReferenceMetadata(metadata) {
@@ -1344,6 +1524,17 @@ function isContributionBranch(build) {
1344
1524
  );
1345
1525
  }
1346
1526
 
1527
+ function resolveContributionActionBuildIds(build) {
1528
+ const contributionBuildId = Number(build?.id || 0);
1529
+ const rootBuildId = Number(build?.contributionRootBuildId || 0);
1530
+ if (!contributionBuildId || !rootBuildId || !isContributionBranch(build)) {
1531
+ throw new Error(
1532
+ "Pass a branch URL such as https://www.twin-kle.com/build/884/4, or run this from a pulled branch workspace.",
1533
+ );
1534
+ }
1535
+ return { rootBuildId, contributionBuildId };
1536
+ }
1537
+
1347
1538
  function printPullResult(result) {
1348
1539
  const build = result.build || {};
1349
1540
  const entryPath = result.manifest?.entryPath || "unknown";
@@ -1356,8 +1547,17 @@ function printPullResult(result) {
1356
1547
  console.log(`Next: cd ${shellQuote(result.dir)}`);
1357
1548
  if (build.canWrite === false) {
1358
1549
  console.log("This checkout is read-only for the current CLI login.");
1550
+ if (isContributionBranch(build)) {
1551
+ console.log("Review changes: lumine diff");
1552
+ if (build.role === "project_owner") {
1553
+ console.log("Owner actions: lumine merge, or lumine replace-main");
1554
+ }
1555
+ }
1359
1556
  return;
1360
1557
  }
1558
+ if (result.fileCount === 0) {
1559
+ console.log("No project files yet. Create /index.html before your first save.");
1560
+ }
1361
1561
  console.log('Codex: codex "Read AGENTS.md, then make the requested change."');
1362
1562
  console.log(
1363
1563
  'Claude Code: claude "Read CLAUDE.md, then make the requested change."',
@@ -1370,6 +1570,12 @@ function printPullResult(result) {
1370
1570
  }
1371
1571
  }
1372
1572
 
1573
+ function printNewBuildResult({ createResult, pullResult }) {
1574
+ const build = pullResult.build || createResult.build || {};
1575
+ console.log(`Created ${formatBuildTitle(build)}.`);
1576
+ printPullResult(pullResult);
1577
+ }
1578
+
1373
1579
  function printReferenceResult(result) {
1374
1580
  const build = result.build || {};
1375
1581
  const sourceBuildId =
@@ -1426,6 +1632,57 @@ function printSaveResult({ result, build, dir, files }) {
1426
1632
  }
1427
1633
  }
1428
1634
 
1635
+ function printContributionDiff({ result, build }) {
1636
+ const summary = result.diff?.summary || {};
1637
+ const files = Array.isArray(result.diff?.changedFiles)
1638
+ ? result.diff.changedFiles
1639
+ : [];
1640
+ const branchNumber = Number(build.contributionBranchNumber || 0) || 0;
1641
+ const branchLabel = branchNumber ? `branch ${branchNumber}` : `branch #${build.id}`;
1642
+ console.log(`Diff for ${branchLabel}:`);
1643
+ console.log(
1644
+ `- total=${summary.total ?? files.length} added=${summary.added ?? 0} ` +
1645
+ `updated=${summary.updated ?? 0} deleted=${summary.deleted ?? 0}`,
1646
+ );
1647
+ if (result.rootDrifted) {
1648
+ console.log("- main changed after this branch was created");
1649
+ }
1650
+ if (!files.length) {
1651
+ console.log("No file changes.");
1652
+ return;
1653
+ }
1654
+ for (const file of files) {
1655
+ const mergeStatus = file.mergeStatus ? ` (${file.mergeStatus})` : "";
1656
+ console.log(`- ${file.status || "changed"} ${file.path}${mergeStatus}`);
1657
+ }
1658
+ }
1659
+
1660
+ function printContributionActionResult({
1661
+ action,
1662
+ result,
1663
+ rootBuildId,
1664
+ contributionBuildId,
1665
+ }) {
1666
+ if (!result?.success) {
1667
+ console.log(result?.error || "Branch action did not complete.");
1668
+ process.exitCode = 1;
1669
+ return;
1670
+ }
1671
+ const projectFiles = Array.isArray(result.projectFiles)
1672
+ ? result.projectFiles
1673
+ : [];
1674
+ console.log(`${action} branch #${contributionBuildId} for Build #${rootBuildId}.`);
1675
+ if (projectFiles.length > 0) {
1676
+ console.log(
1677
+ `Main now has ${projectFiles.length} project file${projectFiles.length === 1 ? "" : "s"}.`,
1678
+ );
1679
+ }
1680
+ if (result.mergeConflictsWritten || result.conflicts?.length > 0) {
1681
+ console.log("Merge wrote conflict markers. Resolve them in the Build workspace.");
1682
+ }
1683
+ console.log(`Main workspace: ${rootBuildId}`);
1684
+ }
1685
+
1429
1686
  async function resolveAuth(options) {
1430
1687
  if (options.authToken) {
1431
1688
  return { token: options.authToken };
@@ -1658,6 +1915,7 @@ function parseArgs(args) {
1658
1915
  const booleanFlags = new Set([
1659
1916
  "noOpen",
1660
1917
  "open",
1918
+ "noDescription",
1661
1919
  "publish",
1662
1920
  "save",
1663
1921
  "noUpdateCheck",
@@ -1688,6 +1946,20 @@ function parseArgs(args) {
1688
1946
  return {
1689
1947
  command,
1690
1948
  target: raw.url || raw.target || positional[0] || "",
1949
+ title:
1950
+ String(
1951
+ raw.title ||
1952
+ (command === "new" ? positional.join(" ") : ""),
1953
+ ).trim() || "",
1954
+ description:
1955
+ Object.prototype.hasOwnProperty.call(raw, "description")
1956
+ ? String(raw.description || "").trim()
1957
+ : null,
1958
+ descriptionProvided: Object.prototype.hasOwnProperty.call(
1959
+ raw,
1960
+ "description",
1961
+ ),
1962
+ noDescription: parseBoolean(raw.noDescription, false),
1691
1963
  searchQuery:
1692
1964
  String(
1693
1965
  raw.search ||
@@ -1745,8 +2017,14 @@ async function resolveRequiredBuildIdOrSelected(
1745
2017
  auth,
1746
2018
  { localProject = null } = {},
1747
2019
  ) {
1748
- const buildId = resolveBuildId(options.target);
1749
- if (buildId > 0) return buildId;
2020
+ const targetReference = resolveBuildReference(options.target);
2021
+ if (targetReference.buildId > 0) {
2022
+ return await resolveBuildReferenceBuildId({
2023
+ options,
2024
+ auth,
2025
+ reference: targetReference,
2026
+ });
2027
+ }
1750
2028
  const resolvedLocalProject =
1751
2029
  localProject ||
1752
2030
  (await findLocalProjectMetadata(
@@ -1774,8 +2052,42 @@ async function resolveRequiredBuildIdOrSelected(
1774
2052
  );
1775
2053
  }
1776
2054
 
2055
+ async function resolveNewBuildTitle(options) {
2056
+ const title = String(options.title || "").trim();
2057
+ if (title) return title;
2058
+ if (!input.isTTY || !output.isTTY) {
2059
+ throw new Error('Pass a title: `lumine new "My Build"` or `lumine new --title "My Build"`.');
2060
+ }
2061
+ const rl = readline.createInterface({ input, output });
2062
+ try {
2063
+ const answer = await rl.question("Build title: ");
2064
+ const normalized = String(answer || "").trim();
2065
+ if (!normalized) {
2066
+ throw new Error("Title is required.");
2067
+ }
2068
+ return normalized;
2069
+ } finally {
2070
+ rl.close();
2071
+ }
2072
+ }
2073
+
2074
+ async function resolveNewBuildDescription(options) {
2075
+ if (options.noDescription) return null;
2076
+ if (options.descriptionProvided) {
2077
+ return String(options.description || "").trim() || null;
2078
+ }
2079
+ if (!input.isTTY || !output.isTTY) return null;
2080
+ const rl = readline.createInterface({ input, output });
2081
+ try {
2082
+ const answer = await rl.question("Description (optional): ");
2083
+ return String(answer || "").trim() || null;
2084
+ } finally {
2085
+ rl.close();
2086
+ }
2087
+ }
2088
+
1777
2089
  function resolveRequiredBuildId(value) {
1778
- const buildId = resolveBuildId(value);
2090
+ const buildId = resolveBuildReference(value).buildId;
1779
2091
  if (buildId > 0) return buildId;
1780
2092
  throw new Error(
1781
2093
  "Pass a Twinkle build URL, app URL, preview URL, or build id.",
@@ -1783,40 +2095,74 @@ function resolveRequiredBuildId(value) {
1783
2095
  }
1784
2096
 
1785
2097
  function resolveBuildId(value) {
2098
+ return resolveBuildReference(value).buildId;
2099
+ }
2100
+
2101
+ async function resolveBuildReferenceBuildId({ options, auth, reference }) {
2102
+ if (reference.branchNumber > 0) {
2103
+ const build = await resolveBranchBuild({
2104
+ options,
2105
+ auth,
2106
+ rootBuildId: reference.buildId,
2107
+ branchNumber: reference.branchNumber,
2108
+ });
2109
+ return Number(build.id || 0);
2110
+ }
2111
+ return Number(reference.buildId || 0);
2112
+ }
2113
+
2114
+ function resolveBuildReference(value) {
1786
2115
  const rawValue = String(value || "").trim();
1787
2116
  const directId = Number(rawValue);
1788
- if (Number.isFinite(directId) && directId > 0) return directId;
1789
- if (!rawValue) return 0;
2117
+ if (Number.isFinite(directId) && directId > 0) {
2118
+ return { buildId: directId, branchNumber: 0 };
2119
+ }
2120
+ if (!rawValue) return { buildId: 0, branchNumber: 0 };
1790
2121
 
1791
2122
  try {
1792
2123
  const parsedUrl = new URL(rawValue);
1793
2124
  const host = parsedUrl.hostname.toLowerCase();
1794
2125
  const previewHost = host.match(/^b-(\d+)\.preview\.lumine\.app$/);
1795
- if (previewHost) return Number(previewHost[1]) || 0;
2126
+ if (previewHost) {
2127
+ return { buildId: Number(previewHost[1]) || 0, branchNumber: 0 };
2128
+ }
1796
2129
 
1797
2130
  const parts = parsedUrl.pathname.split("/").filter(Boolean);
1798
2131
  const appIndex = parts.indexOf("app");
1799
- if (appIndex >= 0) return Number(parts[appIndex + 1]) || 0;
2132
+ if (appIndex >= 0) {
2133
+ return { buildId: Number(parts[appIndex + 1]) || 0, branchNumber: 0 };
2134
+ }
1800
2135
 
1801
2136
  const buildIndex = parts.indexOf("build");
1802
2137
  if (buildIndex >= 0) {
1803
2138
  if (parts[buildIndex + 1] === "preview") {
1804
2139
  const nestedBuildIndex = parts.indexOf("build", buildIndex + 2);
1805
- return Number(parts[nestedBuildIndex + 1]) || 0;
2140
+ return {
2141
+ buildId: Number(parts[nestedBuildIndex + 1]) || 0,
2142
+ branchNumber: 0,
2143
+ };
1806
2144
  }
1807
- return Number(parts[buildIndex + 1]) || 0;
2145
+ return {
2146
+ buildId: Number(parts[buildIndex + 1]) || 0,
2147
+ branchNumber: Number(parts[buildIndex + 2]) || 0,
2148
+ };
1808
2149
  }
1809
2150
 
1810
- return (
1811
- Number(parsedUrl.searchParams.get("buildId")) ||
1812
- Number(parsedUrl.searchParams.get("build")) ||
1813
- 0
1814
- );
2151
+ return {
2152
+ buildId:
2153
+ Number(parsedUrl.searchParams.get("buildId")) ||
2154
+ Number(parsedUrl.searchParams.get("build")) ||
2155
+ 0,
2156
+ branchNumber: 0,
2157
+ };
1815
2158
  } catch {
1816
2159
  const match = rawValue.match(
1817
- /(?:^|\/)(?:app|build)\/(?:preview\/build\/)?(\d+)(?:\/|$)/,
2160
+ /(?:^|\/)(?:app|build)\/(?:preview\/build\/)?(\d+)(?:\/(\d+))?(?:\/|$)/,
1818
2161
  );
1819
- return Number(match?.[1] || 0) || 0;
2162
+ return {
2163
+ buildId: Number(match?.[1] || 0) || 0,
2164
+ branchNumber: Number(match?.[2] || 0) || 0,
2165
+ };
1820
2166
  }
1821
2167
  }
1822
2168
 
@@ -1920,12 +2266,16 @@ function printHelp() {
1920
2266
  lumine login
1921
2267
  lumine whoami
1922
2268
  lumine logout
2269
+ lumine new [title]
1923
2270
  lumine projects
1924
2271
  lumine explore [search terms]
1925
2272
  lumine select [twinkle-build-url]
1926
2273
  lumine pull [twinkle-build-url]
1927
2274
  lumine reference <twinkle-build-url>
1928
2275
  lumine fork <twinkle-build-url>
2276
+ lumine diff <twinkle-branch-url>
2277
+ lumine merge <twinkle-branch-url>
2278
+ lumine replace-main <twinkle-branch-url>
1929
2279
  lumine save
1930
2280
  lumine check [twinkle-build-url]
1931
2281
  lumine launch [twinkle-build-url]
@@ -1933,9 +2283,13 @@ function printHelp() {
1933
2283
  Examples:
1934
2284
  npx @stage5/lumine@latest
1935
2285
  npx @stage5/lumine@latest login
2286
+ npx @stage5/lumine@latest new "Daily Reflection App"
2287
+ npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
1936
2288
  npx @stage5/lumine@latest explore --sort forks
1937
2289
  npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
1938
2290
  npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
2291
+ npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
2292
+ npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
1939
2293
  npx @stage5/lumine@latest pull
1940
2294
  npx @stage5/lumine@latest save
1941
2295
  npx @stage5/lumine@latest save --publish
@@ -1948,6 +2302,9 @@ Options:
1948
2302
  --auth-file <path> Saved login path
1949
2303
  --auth-token <token> Override saved login
1950
2304
  --dir <path> Directory for pulled project files
2305
+ --title <text> New Build title
2306
+ --description <text> Optional New Build description
2307
+ --no-description Skip the New Build description prompt
1951
2308
  --summary <text> Save summary
1952
2309
  --search <text> Search public open-source Builds
1953
2310
  --sort <sort> Sort open-source Builds: forks, popular, recent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.25.0
3
+ Version: 1.25.1
4
4
  Updated: 2026-05-28
5
- Generated: 2026-05-28T09:34:12.631Z
5
+ Generated: 2026-05-28T10:24:18.784Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -203,15 +203,15 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
203
203
 
204
204
  ### Twinkle.aiCards
205
205
  - async list({ limit, cursor, level, minLevel, maxLevel, quality, userId, hasImage, hasExample } = {}) | scopes: content:read
206
- - Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
206
+ - Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, isMysteryCard, isImageGenerating, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
207
207
  - List existing public AI Cards newest first, including each card word, example sentence text, word level, and quality.
208
208
  - Example: const { cards } = await Twinkle.aiCards.list({ level: 2, hasExample: true, limit: 20 });
209
209
  - async search({ query, limit, cursor, level, minLevel, maxLevel, quality, userId, hasImage, hasExample } = {}) | scopes: content:read
210
- - Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
210
+ - Returns: { cards: [{ id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, isMysteryCard, isImageGenerating, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
211
211
  - Search existing public AI Cards by word, with optional level and quality filters.
212
212
  - Example: const { cards } = await Twinkle.aiCards.search({ query: searchText, minLevel: 1, maxLevel: 3, hasExample: true, limit: 12 });
213
213
  - async get(cardId) | scopes: content:read
214
- - Returns: { card: { id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction } }
214
+ - Returns: { card: { id, contentType, contentId, word, text, exampleText, prompt, level, wordLevel, quality, style, imagePath, imageUrl, isMysteryCard, isImageGenerating, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction } }
215
215
  - Fetch one existing public AI Card by id, including word, example text, and level metadata.
216
216
  - Example: const { card } = await Twinkle.aiCards.get(cardId);
217
217