@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 +9 -0
- package/bin/lumine.js +385 -28
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +5 -5
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
1749
|
-
if (buildId > 0)
|
|
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 =
|
|
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)
|
|
1789
|
-
|
|
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)
|
|
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)
|
|
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
|
|
2140
|
+
return {
|
|
2141
|
+
buildId: Number(parts[nestedBuildIndex + 1]) || 0,
|
|
2142
|
+
branchNumber: 0,
|
|
2143
|
+
};
|
|
1806
2144
|
}
|
|
1807
|
-
return
|
|
2145
|
+
return {
|
|
2146
|
+
buildId: Number(parts[buildIndex + 1]) || 0,
|
|
2147
|
+
branchNumber: Number(parts[buildIndex + 2]) || 0,
|
|
2148
|
+
};
|
|
1808
2149
|
}
|
|
1809
2150
|
|
|
1810
|
-
return
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
|
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
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.25.
|
|
3
|
+
Version: 1.25.1
|
|
4
4
|
Updated: 2026-05-28
|
|
5
|
-
Generated: 2026-05-
|
|
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
|
|