@stage5/lumine 0.1.5 → 0.1.7
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/bin/lumine.js +285 -28
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +54 -6
package/bin/lumine.js
CHANGED
|
@@ -50,6 +50,8 @@ Use these current source-of-truth rules:
|
|
|
50
50
|
- Use Twinkle.privateDb for simple private per-user preferences, drafts, settings, and small JSON state.
|
|
51
51
|
- Use Twinkle.userDb only for advanced private SQLite tables, indexes, many rows, filtered queries, or aggregates.
|
|
52
52
|
- Use Twinkle.sharedDb for shared multi-user JSON data.
|
|
53
|
+
- Use Twinkle.aiCards.list/search/get for existing public AI Card words, exampleText sentence material, and word levels.
|
|
54
|
+
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
53
55
|
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
|
|
54
56
|
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
55
57
|
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
@@ -123,6 +125,9 @@ const COMMANDS = new Set([
|
|
|
123
125
|
"pull",
|
|
124
126
|
"reference",
|
|
125
127
|
"fork",
|
|
128
|
+
"diff",
|
|
129
|
+
"merge",
|
|
130
|
+
"replace-main",
|
|
126
131
|
"save",
|
|
127
132
|
"push",
|
|
128
133
|
"check",
|
|
@@ -186,6 +191,18 @@ async function main() {
|
|
|
186
191
|
await fork(options);
|
|
187
192
|
return;
|
|
188
193
|
}
|
|
194
|
+
if (options.command === "diff") {
|
|
195
|
+
await diff(options);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (options.command === "merge") {
|
|
199
|
+
await mergeBranch(options);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (options.command === "replace-main") {
|
|
203
|
+
await replaceMainWithBranch(options);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
189
206
|
if (options.command === "save" || options.command === "push") {
|
|
190
207
|
await save(options);
|
|
191
208
|
return;
|
|
@@ -300,11 +317,7 @@ async function whoami(options) {
|
|
|
300
317
|
async function workspace(options) {
|
|
301
318
|
const auth = await ensureAuth(options);
|
|
302
319
|
const selectedBuild = options.target
|
|
303
|
-
? await
|
|
304
|
-
options,
|
|
305
|
-
auth,
|
|
306
|
-
buildId: resolveRequiredBuildId(options.target),
|
|
307
|
-
})
|
|
320
|
+
? await loadTargetBuildMetadata({ options, auth })
|
|
308
321
|
: await chooseProject({
|
|
309
322
|
builds: await listBuilds({ options, auth }),
|
|
310
323
|
});
|
|
@@ -335,11 +348,7 @@ async function explore(options) {
|
|
|
335
348
|
async function selectProject(options) {
|
|
336
349
|
const auth = await resolveAuth(options);
|
|
337
350
|
const selectedBuild = options.target
|
|
338
|
-
? await
|
|
339
|
-
options,
|
|
340
|
-
auth,
|
|
341
|
-
buildId: resolveRequiredBuildId(options.target),
|
|
342
|
-
})
|
|
351
|
+
? await loadTargetBuildMetadata({ options, auth })
|
|
343
352
|
: await chooseProject({
|
|
344
353
|
builds: await listBuilds({ options, auth }),
|
|
345
354
|
});
|
|
@@ -374,7 +383,7 @@ async function pull(options) {
|
|
|
374
383
|
|
|
375
384
|
async function reference(options) {
|
|
376
385
|
const auth = await resolveAuth(options);
|
|
377
|
-
const buildId =
|
|
386
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
378
387
|
const result = await pullReferenceFiles({ options, auth, buildId });
|
|
379
388
|
printReferenceResult(result);
|
|
380
389
|
}
|
|
@@ -382,7 +391,7 @@ async function reference(options) {
|
|
|
382
391
|
async function fork(options) {
|
|
383
392
|
const auth = await resolveAuth(options);
|
|
384
393
|
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
385
|
-
const buildId =
|
|
394
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
386
395
|
const forkResult = await forkBuild({ options, auth, buildId });
|
|
387
396
|
const forkedBuildId = Number(forkResult.build?.id || 0);
|
|
388
397
|
if (!forkedBuildId) {
|
|
@@ -397,6 +406,60 @@ async function fork(options) {
|
|
|
397
406
|
printForkResult({ forkResult, pullResult: result });
|
|
398
407
|
}
|
|
399
408
|
|
|
409
|
+
async function diff(options) {
|
|
410
|
+
const auth = await resolveAuth(options);
|
|
411
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
412
|
+
const { rootBuildId, contributionBuildId } =
|
|
413
|
+
resolveContributionActionBuildIds(build);
|
|
414
|
+
const result = await loadContributionDiff({
|
|
415
|
+
options,
|
|
416
|
+
auth,
|
|
417
|
+
rootBuildId,
|
|
418
|
+
contributionBuildId,
|
|
419
|
+
});
|
|
420
|
+
printContributionDiff({ result, build });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function mergeBranch(options) {
|
|
424
|
+
const auth = await resolveAuth(options);
|
|
425
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
426
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
427
|
+
const { rootBuildId, contributionBuildId } =
|
|
428
|
+
resolveContributionActionBuildIds(build);
|
|
429
|
+
const result = await mergeContributionIntoMain({
|
|
430
|
+
options,
|
|
431
|
+
auth,
|
|
432
|
+
rootBuildId,
|
|
433
|
+
contributionBuildId,
|
|
434
|
+
});
|
|
435
|
+
printContributionActionResult({
|
|
436
|
+
action: "Merged",
|
|
437
|
+
result,
|
|
438
|
+
rootBuildId,
|
|
439
|
+
contributionBuildId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function replaceMainWithBranch(options) {
|
|
444
|
+
const auth = await resolveAuth(options);
|
|
445
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
446
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
447
|
+
const { rootBuildId, contributionBuildId } =
|
|
448
|
+
resolveContributionActionBuildIds(build);
|
|
449
|
+
const result = await replaceMainWithContribution({
|
|
450
|
+
options,
|
|
451
|
+
auth,
|
|
452
|
+
rootBuildId,
|
|
453
|
+
contributionBuildId,
|
|
454
|
+
});
|
|
455
|
+
printContributionActionResult({
|
|
456
|
+
action: "Replaced main with",
|
|
457
|
+
result,
|
|
458
|
+
rootBuildId,
|
|
459
|
+
contributionBuildId,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
400
463
|
async function save(options) {
|
|
401
464
|
const auth = await resolveAuth(options);
|
|
402
465
|
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
@@ -413,6 +476,11 @@ async function save(options) {
|
|
|
413
476
|
buildId,
|
|
414
477
|
localProject,
|
|
415
478
|
});
|
|
479
|
+
if (build?.canWrite === false) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
"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.",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
416
484
|
buildId = Number(build?.id || buildId);
|
|
417
485
|
const dir = resolveProjectDirForSave({ options, localProject });
|
|
418
486
|
const files = await collectProjectFiles(dir);
|
|
@@ -564,6 +632,11 @@ async function publishBuild({ options, buildId, auth }) {
|
|
|
564
632
|
function printCheck(result) {
|
|
565
633
|
const checks = result.checks || {};
|
|
566
634
|
console.log(`Launch check: ${result.ok ? "ok" : "fail"}`);
|
|
635
|
+
if (checks.canonicalBuild) {
|
|
636
|
+
console.log(
|
|
637
|
+
`- canonical build: ${checks.canonicalBuild.ok ? "ok" : "fail"}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
567
640
|
console.log(
|
|
568
641
|
`- project files: ${checks.projectFiles?.ok ? "ok" : "fail"} ` +
|
|
569
642
|
`files=${checks.projectFiles?.fileCount ?? 0}`,
|
|
@@ -643,6 +716,11 @@ async function loadBuildMetadata({ options, auth, buildId }) {
|
|
|
643
716
|
return result.build;
|
|
644
717
|
}
|
|
645
718
|
|
|
719
|
+
async function loadTargetBuildMetadata({ options, auth }) {
|
|
720
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
721
|
+
return await loadBuildMetadata({ options, auth, buildId });
|
|
722
|
+
}
|
|
723
|
+
|
|
646
724
|
async function loadOpenSourceBuildFiles({
|
|
647
725
|
options,
|
|
648
726
|
auth,
|
|
@@ -668,6 +746,24 @@ async function loadBuildFiles({ options, auth, buildId, includeContent }) {
|
|
|
668
746
|
});
|
|
669
747
|
}
|
|
670
748
|
|
|
749
|
+
async function resolveBranchBuild({
|
|
750
|
+
options,
|
|
751
|
+
auth,
|
|
752
|
+
rootBuildId,
|
|
753
|
+
branchNumber,
|
|
754
|
+
}) {
|
|
755
|
+
const result = await requestJson({
|
|
756
|
+
url: `${options.apiUrl}/cli/build/${rootBuildId}/branches/${branchNumber}`,
|
|
757
|
+
authToken: auth.token,
|
|
758
|
+
timeoutMs: options.timeoutMs,
|
|
759
|
+
});
|
|
760
|
+
const branchBuildId = Number(result.build?.id || 0);
|
|
761
|
+
if (!branchBuildId) {
|
|
762
|
+
throw new Error(`Branch ${branchNumber} for Build ${rootBuildId} could not be loaded.`);
|
|
763
|
+
}
|
|
764
|
+
return result.build;
|
|
765
|
+
}
|
|
766
|
+
|
|
671
767
|
async function forkBuild({ options, auth, buildId }) {
|
|
672
768
|
return await requestJson({
|
|
673
769
|
method: "POST",
|
|
@@ -692,6 +788,49 @@ async function saveProjectFiles({ options, auth, buildId, files, summary }) {
|
|
|
692
788
|
});
|
|
693
789
|
}
|
|
694
790
|
|
|
791
|
+
async function loadContributionDiff({
|
|
792
|
+
options,
|
|
793
|
+
auth,
|
|
794
|
+
rootBuildId,
|
|
795
|
+
contributionBuildId,
|
|
796
|
+
}) {
|
|
797
|
+
return await requestJson({
|
|
798
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}`,
|
|
799
|
+
authToken: auth.token,
|
|
800
|
+
timeoutMs: options.timeoutMs,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function mergeContributionIntoMain({
|
|
805
|
+
options,
|
|
806
|
+
auth,
|
|
807
|
+
rootBuildId,
|
|
808
|
+
contributionBuildId,
|
|
809
|
+
}) {
|
|
810
|
+
return await requestJson({
|
|
811
|
+
method: "POST",
|
|
812
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/merge`,
|
|
813
|
+
authToken: auth.token,
|
|
814
|
+
body: {},
|
|
815
|
+
timeoutMs: options.timeoutMs,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function replaceMainWithContribution({
|
|
820
|
+
options,
|
|
821
|
+
auth,
|
|
822
|
+
rootBuildId,
|
|
823
|
+
contributionBuildId,
|
|
824
|
+
}) {
|
|
825
|
+
return await requestJson({
|
|
826
|
+
method: "POST",
|
|
827
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/replace-main`,
|
|
828
|
+
authToken: auth.token,
|
|
829
|
+
body: {},
|
|
830
|
+
timeoutMs: options.timeoutMs,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
695
834
|
async function resolveBuildForSave({ options, auth, buildId, localProject }) {
|
|
696
835
|
const localBuild = localProject?.metadata?.build;
|
|
697
836
|
const localBuildId =
|
|
@@ -1224,6 +1363,11 @@ function assertLocalProjectCanBeSaved(localProject) {
|
|
|
1224
1363
|
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
|
|
1225
1364
|
);
|
|
1226
1365
|
}
|
|
1366
|
+
if (metadata.build?.canWrite === false) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
"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.",
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1227
1371
|
}
|
|
1228
1372
|
|
|
1229
1373
|
function isReadOnlyReferenceMetadata(metadata) {
|
|
@@ -1342,6 +1486,17 @@ function isContributionBranch(build) {
|
|
|
1342
1486
|
);
|
|
1343
1487
|
}
|
|
1344
1488
|
|
|
1489
|
+
function resolveContributionActionBuildIds(build) {
|
|
1490
|
+
const contributionBuildId = Number(build?.id || 0);
|
|
1491
|
+
const rootBuildId = Number(build?.contributionRootBuildId || 0);
|
|
1492
|
+
if (!contributionBuildId || !rootBuildId || !isContributionBranch(build)) {
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
"Pass a branch URL such as https://www.twin-kle.com/build/884/4, or run this from a pulled branch workspace.",
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
return { rootBuildId, contributionBuildId };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1345
1500
|
function printPullResult(result) {
|
|
1346
1501
|
const build = result.build || {};
|
|
1347
1502
|
const entryPath = result.manifest?.entryPath || "unknown";
|
|
@@ -1354,6 +1509,12 @@ function printPullResult(result) {
|
|
|
1354
1509
|
console.log(`Next: cd ${shellQuote(result.dir)}`);
|
|
1355
1510
|
if (build.canWrite === false) {
|
|
1356
1511
|
console.log("This checkout is read-only for the current CLI login.");
|
|
1512
|
+
if (isContributionBranch(build)) {
|
|
1513
|
+
console.log("Review changes: lumine diff");
|
|
1514
|
+
if (build.role === "project_owner") {
|
|
1515
|
+
console.log("Owner actions: lumine merge, or lumine replace-main");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1357
1518
|
return;
|
|
1358
1519
|
}
|
|
1359
1520
|
console.log('Codex: codex "Read AGENTS.md, then make the requested change."');
|
|
@@ -1424,6 +1585,57 @@ function printSaveResult({ result, build, dir, files }) {
|
|
|
1424
1585
|
}
|
|
1425
1586
|
}
|
|
1426
1587
|
|
|
1588
|
+
function printContributionDiff({ result, build }) {
|
|
1589
|
+
const summary = result.diff?.summary || {};
|
|
1590
|
+
const files = Array.isArray(result.diff?.changedFiles)
|
|
1591
|
+
? result.diff.changedFiles
|
|
1592
|
+
: [];
|
|
1593
|
+
const branchNumber = Number(build.contributionBranchNumber || 0) || 0;
|
|
1594
|
+
const branchLabel = branchNumber ? `branch ${branchNumber}` : `branch #${build.id}`;
|
|
1595
|
+
console.log(`Diff for ${branchLabel}:`);
|
|
1596
|
+
console.log(
|
|
1597
|
+
`- total=${summary.total ?? files.length} added=${summary.added ?? 0} ` +
|
|
1598
|
+
`updated=${summary.updated ?? 0} deleted=${summary.deleted ?? 0}`,
|
|
1599
|
+
);
|
|
1600
|
+
if (result.rootDrifted) {
|
|
1601
|
+
console.log("- main changed after this branch was created");
|
|
1602
|
+
}
|
|
1603
|
+
if (!files.length) {
|
|
1604
|
+
console.log("No file changes.");
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
for (const file of files) {
|
|
1608
|
+
const mergeStatus = file.mergeStatus ? ` (${file.mergeStatus})` : "";
|
|
1609
|
+
console.log(`- ${file.status || "changed"} ${file.path}${mergeStatus}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function printContributionActionResult({
|
|
1614
|
+
action,
|
|
1615
|
+
result,
|
|
1616
|
+
rootBuildId,
|
|
1617
|
+
contributionBuildId,
|
|
1618
|
+
}) {
|
|
1619
|
+
if (!result?.success) {
|
|
1620
|
+
console.log(result?.error || "Branch action did not complete.");
|
|
1621
|
+
process.exitCode = 1;
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const projectFiles = Array.isArray(result.projectFiles)
|
|
1625
|
+
? result.projectFiles
|
|
1626
|
+
: [];
|
|
1627
|
+
console.log(`${action} branch #${contributionBuildId} for Build #${rootBuildId}.`);
|
|
1628
|
+
if (projectFiles.length > 0) {
|
|
1629
|
+
console.log(
|
|
1630
|
+
`Main now has ${projectFiles.length} project file${projectFiles.length === 1 ? "" : "s"}.`,
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
if (result.mergeConflictsWritten || result.conflicts?.length > 0) {
|
|
1634
|
+
console.log("Merge wrote conflict markers. Resolve them in the Build workspace.");
|
|
1635
|
+
}
|
|
1636
|
+
console.log(`Main workspace: ${rootBuildId}`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1427
1639
|
async function resolveAuth(options) {
|
|
1428
1640
|
if (options.authToken) {
|
|
1429
1641
|
return { token: options.authToken };
|
|
@@ -1743,8 +1955,14 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
1743
1955
|
auth,
|
|
1744
1956
|
{ localProject = null } = {},
|
|
1745
1957
|
) {
|
|
1746
|
-
const
|
|
1747
|
-
if (buildId > 0)
|
|
1958
|
+
const targetReference = resolveBuildReference(options.target);
|
|
1959
|
+
if (targetReference.buildId > 0) {
|
|
1960
|
+
return await resolveBuildReferenceBuildId({
|
|
1961
|
+
options,
|
|
1962
|
+
auth,
|
|
1963
|
+
reference: targetReference,
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1748
1966
|
const resolvedLocalProject =
|
|
1749
1967
|
localProject ||
|
|
1750
1968
|
(await findLocalProjectMetadata(
|
|
@@ -1773,7 +1991,7 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
1773
1991
|
}
|
|
1774
1992
|
|
|
1775
1993
|
function resolveRequiredBuildId(value) {
|
|
1776
|
-
const buildId =
|
|
1994
|
+
const buildId = resolveBuildReference(value).buildId;
|
|
1777
1995
|
if (buildId > 0) return buildId;
|
|
1778
1996
|
throw new Error(
|
|
1779
1997
|
"Pass a Twinkle build URL, app URL, preview URL, or build id.",
|
|
@@ -1781,40 +1999,74 @@ function resolveRequiredBuildId(value) {
|
|
|
1781
1999
|
}
|
|
1782
2000
|
|
|
1783
2001
|
function resolveBuildId(value) {
|
|
2002
|
+
return resolveBuildReference(value).buildId;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
async function resolveBuildReferenceBuildId({ options, auth, reference }) {
|
|
2006
|
+
if (reference.branchNumber > 0) {
|
|
2007
|
+
const build = await resolveBranchBuild({
|
|
2008
|
+
options,
|
|
2009
|
+
auth,
|
|
2010
|
+
rootBuildId: reference.buildId,
|
|
2011
|
+
branchNumber: reference.branchNumber,
|
|
2012
|
+
});
|
|
2013
|
+
return Number(build.id || 0);
|
|
2014
|
+
}
|
|
2015
|
+
return Number(reference.buildId || 0);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function resolveBuildReference(value) {
|
|
1784
2019
|
const rawValue = String(value || "").trim();
|
|
1785
2020
|
const directId = Number(rawValue);
|
|
1786
|
-
if (Number.isFinite(directId) && directId > 0)
|
|
1787
|
-
|
|
2021
|
+
if (Number.isFinite(directId) && directId > 0) {
|
|
2022
|
+
return { buildId: directId, branchNumber: 0 };
|
|
2023
|
+
}
|
|
2024
|
+
if (!rawValue) return { buildId: 0, branchNumber: 0 };
|
|
1788
2025
|
|
|
1789
2026
|
try {
|
|
1790
2027
|
const parsedUrl = new URL(rawValue);
|
|
1791
2028
|
const host = parsedUrl.hostname.toLowerCase();
|
|
1792
2029
|
const previewHost = host.match(/^b-(\d+)\.preview\.lumine\.app$/);
|
|
1793
|
-
if (previewHost)
|
|
2030
|
+
if (previewHost) {
|
|
2031
|
+
return { buildId: Number(previewHost[1]) || 0, branchNumber: 0 };
|
|
2032
|
+
}
|
|
1794
2033
|
|
|
1795
2034
|
const parts = parsedUrl.pathname.split("/").filter(Boolean);
|
|
1796
2035
|
const appIndex = parts.indexOf("app");
|
|
1797
|
-
if (appIndex >= 0)
|
|
2036
|
+
if (appIndex >= 0) {
|
|
2037
|
+
return { buildId: Number(parts[appIndex + 1]) || 0, branchNumber: 0 };
|
|
2038
|
+
}
|
|
1798
2039
|
|
|
1799
2040
|
const buildIndex = parts.indexOf("build");
|
|
1800
2041
|
if (buildIndex >= 0) {
|
|
1801
2042
|
if (parts[buildIndex + 1] === "preview") {
|
|
1802
2043
|
const nestedBuildIndex = parts.indexOf("build", buildIndex + 2);
|
|
1803
|
-
return
|
|
2044
|
+
return {
|
|
2045
|
+
buildId: Number(parts[nestedBuildIndex + 1]) || 0,
|
|
2046
|
+
branchNumber: 0,
|
|
2047
|
+
};
|
|
1804
2048
|
}
|
|
1805
|
-
return
|
|
2049
|
+
return {
|
|
2050
|
+
buildId: Number(parts[buildIndex + 1]) || 0,
|
|
2051
|
+
branchNumber: Number(parts[buildIndex + 2]) || 0,
|
|
2052
|
+
};
|
|
1806
2053
|
}
|
|
1807
2054
|
|
|
1808
|
-
return
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2055
|
+
return {
|
|
2056
|
+
buildId:
|
|
2057
|
+
Number(parsedUrl.searchParams.get("buildId")) ||
|
|
2058
|
+
Number(parsedUrl.searchParams.get("build")) ||
|
|
2059
|
+
0,
|
|
2060
|
+
branchNumber: 0,
|
|
2061
|
+
};
|
|
1813
2062
|
} catch {
|
|
1814
2063
|
const match = rawValue.match(
|
|
1815
|
-
/(?:^|\/)(?:app|build)\/(?:preview\/build\/)?(\d+)(?:\/|$)/,
|
|
2064
|
+
/(?:^|\/)(?:app|build)\/(?:preview\/build\/)?(\d+)(?:\/(\d+))?(?:\/|$)/,
|
|
1816
2065
|
);
|
|
1817
|
-
return
|
|
2066
|
+
return {
|
|
2067
|
+
buildId: Number(match?.[1] || 0) || 0,
|
|
2068
|
+
branchNumber: Number(match?.[2] || 0) || 0,
|
|
2069
|
+
};
|
|
1818
2070
|
}
|
|
1819
2071
|
}
|
|
1820
2072
|
|
|
@@ -1924,6 +2176,9 @@ function printHelp() {
|
|
|
1924
2176
|
lumine pull [twinkle-build-url]
|
|
1925
2177
|
lumine reference <twinkle-build-url>
|
|
1926
2178
|
lumine fork <twinkle-build-url>
|
|
2179
|
+
lumine diff <twinkle-branch-url>
|
|
2180
|
+
lumine merge <twinkle-branch-url>
|
|
2181
|
+
lumine replace-main <twinkle-branch-url>
|
|
1927
2182
|
lumine save
|
|
1928
2183
|
lumine check [twinkle-build-url]
|
|
1929
2184
|
lumine launch [twinkle-build-url]
|
|
@@ -1934,6 +2189,8 @@ Examples:
|
|
|
1934
2189
|
npx @stage5/lumine@latest explore --sort forks
|
|
1935
2190
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
1936
2191
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
2192
|
+
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
2193
|
+
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
1937
2194
|
npx @stage5/lumine@latest pull
|
|
1938
2195
|
npx @stage5/lumine@latest save
|
|
1939
2196
|
npx @stage5/lumine@latest save --publish
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-05-
|
|
5
|
-
Generated: 2026-05-
|
|
3
|
+
Version: 1.25.1
|
|
4
|
+
Updated: 2026-05-28
|
|
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.
|
|
@@ -13,6 +13,7 @@ Generated: 2026-05-26T10:20:06.718Z
|
|
|
13
13
|
- Use Twinkle.leaderboards for public Build scoreboards. Signed-in viewers are ranked by Twinkle username; guests can submit with a display name.
|
|
14
14
|
- Use Twinkle.sharedDb for custom shared multi-user structured data, guestbooks, votes, and append-only run history.
|
|
15
15
|
- Use Twinkle.subjects.search for in-app subject pickers. Twinkle.mount remains an optional host-provided preselection/context shortcut, not a data API.
|
|
16
|
+
- Use Twinkle.aiCards for read-only existing public AI Card words and example texts, including word levels for typing games.
|
|
16
17
|
- Use Twinkle.aiStories for read-only existing AI Story galleries, readers, quizzes, and remix tools.
|
|
17
18
|
- Use Twinkle.grammarbles for public Grammarbles question-bank trainer apps and optional signed-in viewer attempt-history filtering.
|
|
18
19
|
- Use Twinkle.chess for chess engine play and analysis; app code still owns chess rules, legal moves, board state, and UI.
|
|
@@ -200,18 +201,32 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
200
201
|
- async getSubjectComments(subjectId, { limit, cursor } = {}) | scopes: content:read
|
|
201
202
|
- Returns: { comments: [{ id, content, filePath, fileName, fileSize, thumbUrl, timeStamp }], cursor? }
|
|
202
203
|
|
|
204
|
+
### Twinkle.aiCards
|
|
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, isMysteryCard, isImageGenerating, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
207
|
+
- List existing public AI Cards newest first, including each card word, example sentence text, word level, and quality.
|
|
208
|
+
- Example: const { cards } = await Twinkle.aiCards.list({ level: 2, hasExample: true, limit: 20 });
|
|
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, isMysteryCard, isImageGenerating, creatorId, ownerId, username, profilePicUrl, timeStamp, lastInteraction }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
211
|
+
- Search existing public AI Cards by word, with optional level and quality filters.
|
|
212
|
+
- Example: const { cards } = await Twinkle.aiCards.search({ query: searchText, minLevel: 1, maxLevel: 3, hasExample: true, limit: 12 });
|
|
213
|
+
- async get(cardId) | scopes: content:read
|
|
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
|
+
- Fetch one existing public AI Card by id, including word, example text, and level metadata.
|
|
216
|
+
- Example: const { card } = await Twinkle.aiCards.get(cardId);
|
|
217
|
+
|
|
203
218
|
### Twinkle.aiStories
|
|
204
219
|
- async list({ limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
205
220
|
- Returns: { stories: [{ id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
206
|
-
- List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, and image/story collections.
|
|
221
|
+
- List completed existing user-generated AI Stories newest first for galleries, quiz apps, readers, passage typing, and image/story collections.
|
|
207
222
|
- Example: const { stories } = await Twinkle.aiStories.list({ hasImage: true, hasQuestions: true, limit: 12 });
|
|
208
223
|
- async search({ query, limit, cursor, difficulty, type, isListening, userId, hasImage, hasQuestions } = {}) | scopes: content:read
|
|
209
224
|
- Returns: { stories: [{ id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp }], cursor?, pagination: { limit, hasMore, nextCursor }, filters }
|
|
210
|
-
- Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, and
|
|
225
|
+
- Search completed existing user-generated AI Stories by topic or story text for galleries, quizzes, readers, and passage typing games.
|
|
211
226
|
- Example: const { stories } = await Twinkle.aiStories.search({ query: searchText, hasQuestions: true, limit: 12 });
|
|
212
227
|
- async get(storyId) | scopes: content:read
|
|
213
228
|
- Returns: { story: { id, contentType, contentId, topic, topicKey, type, story, explanation, difficulty, isListening, imagePath, imageUrl, audioPath, audioUrl, questions, questionsBy, hasImage, hasQuestions, userId, username, profilePicUrl, timeStamp } }
|
|
214
|
-
- Fetch one completed existing AI Story by id, including story text, media URLs, and normalized questions when available.
|
|
229
|
+
- Fetch one completed existing AI Story by id, including story text for passage typing, media URLs, and normalized questions when available.
|
|
215
230
|
- Example: const { story } = await Twinkle.aiStories.get(storyId);
|
|
216
231
|
|
|
217
232
|
### Twinkle.grammarbles
|
|
@@ -302,8 +317,10 @@ world.updatePresence({ x, y, z, facing });
|
|
|
302
317
|
### Twinkle.reflections
|
|
303
318
|
- async getDailyReflections({ userIds, cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
304
319
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
|
|
320
|
+
- Read only currently public Daily Reflection shares. response is the exact text the author chose to share publicly, never an unshared raw/private answer.
|
|
305
321
|
- async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
306
322
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question }], cursor? }
|
|
323
|
+
- Read one user's currently public Daily Reflection shares. response is the exact text the author chose to share publicly, never an unshared raw/private answer.
|
|
307
324
|
|
|
308
325
|
### Twinkle.privateDb
|
|
309
326
|
- async get(key) | scopes: privateDb:read
|
|
@@ -385,6 +402,37 @@ const { comments, pagination } = await Twinkle.subjectComments.list(subjectId, {
|
|
|
385
402
|
console.log('Title:', subject.title, 'Pages:', comments.length, 'hasMore:', pagination?.hasMore);
|
|
386
403
|
```
|
|
387
404
|
|
|
405
|
+
### Typing game content from AI Cards and AI Stories
|
|
406
|
+
Use AI Card words for word mode, AI Card exampleText for sentence mode, and AI Story story text for passage mode. Keep leaderboards separate per mode.
|
|
407
|
+
Keywords: typing, ai cards, ai stories, words, sentences, passages, leaderboard
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
const wordPage = await Twinkle.aiCards.list({ level: 1, hasExample: true, limit: 20 });
|
|
411
|
+
const wordTargets = wordPage.cards.map((card) => ({
|
|
412
|
+
text: card.word,
|
|
413
|
+
level: card.level,
|
|
414
|
+
sourceId: card.id
|
|
415
|
+
}));
|
|
416
|
+
const sentenceTargets = wordPage.cards.map((card) => ({
|
|
417
|
+
text: card.exampleText,
|
|
418
|
+
level: card.level,
|
|
419
|
+
sourceId: card.id
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
const passagePage = await Twinkle.aiStories.list({ difficulty: 2, limit: 10 });
|
|
423
|
+
const passageTargets = passagePage.stories.map((story) => ({
|
|
424
|
+
text: story.story,
|
|
425
|
+
level: story.difficulty,
|
|
426
|
+
sourceId: story.id
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
await Twinkle.leaderboards.submit({
|
|
430
|
+
boardKey: 'arcade-typing-words',
|
|
431
|
+
score: finalScore,
|
|
432
|
+
meta: { mode: 'words', level }
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
388
436
|
### Search AI Stories for a quiz app
|
|
389
437
|
Use existing user-generated AI Stories as source material for visual galleries, readers, and question-based games.
|
|
390
438
|
Keywords: ai stories, story, quiz, questions, image, gallery
|