@stage5/lumine 0.1.3 → 0.1.4
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 +14 -0
- package/bin/lumine.js +323 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ Launch Twinkle Lumine builds from any terminal.
|
|
|
6
6
|
npx @stage5/lumine@latest
|
|
7
7
|
npx @stage5/lumine@latest login
|
|
8
8
|
npx @stage5/lumine@latest projects
|
|
9
|
+
npx @stage5/lumine@latest explore --sort forks
|
|
10
|
+
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
11
|
+
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
9
12
|
npx @stage5/lumine@latest pull
|
|
10
13
|
npx @stage5/lumine@latest save
|
|
11
14
|
npx @stage5/lumine@latest save --publish
|
|
@@ -21,6 +24,13 @@ pulling the owner's main project creates or reuses your contribution branch and
|
|
|
21
24
|
checks out that branch locally. Saves go to your branch, so the project owner
|
|
22
25
|
can merge or replace main from Twinkle.
|
|
23
26
|
|
|
27
|
+
Use `lumine explore` to list public open-source Build apps that can be used as
|
|
28
|
+
examples or starting points. It supports `--search` and `--sort forks`,
|
|
29
|
+
`--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
|
|
30
|
+
to pull source files into a read-only reference folder, or
|
|
31
|
+
`lumine fork <build-url-or-id>` to create your own editable fork and pull it
|
|
32
|
+
locally.
|
|
33
|
+
|
|
24
34
|
After editing pulled files, run `lumine save` from that folder. The CLI saves
|
|
25
35
|
through Twinkle's normal workspace project-file route, creates a project artifact
|
|
26
36
|
version, records the same save metadata, and marks public builds as having
|
|
@@ -35,6 +45,10 @@ uploaded by `lumine save`. Build apps run in sandboxed iframes without native
|
|
|
35
45
|
form submission, so use JavaScript-handled inputs and buttons instead of
|
|
36
46
|
`<form>` elements.
|
|
37
47
|
|
|
48
|
+
Reference folders are marked `readOnly` in `.twinkle/lumine-project.json`.
|
|
49
|
+
Running `lumine save` from a reference folder is blocked; fork the source Build
|
|
50
|
+
first if you want an editable workspace.
|
|
51
|
+
|
|
38
52
|
After pulling a project, run an agent from the pulled folder:
|
|
39
53
|
|
|
40
54
|
```bash
|
package/bin/lumine.js
CHANGED
|
@@ -29,6 +29,8 @@ const EXCLUDED_UPLOAD_FILES = new Set([
|
|
|
29
29
|
const LUMINE_AGENT_INSTRUCTIONS_MARKER =
|
|
30
30
|
"<!-- Lumine CLI Agent Instructions -->";
|
|
31
31
|
const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
|
|
32
|
+
const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
33
|
+
"<!-- Lumine CLI Reference Instructions -->";
|
|
32
34
|
const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
33
35
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
34
36
|
import.meta.url,
|
|
@@ -90,6 +92,20 @@ lumine save --summary "Describe the change"
|
|
|
90
92
|
Report the changed files, any SDK methods used, the lumine save result, the
|
|
91
93
|
build or branch id, and whether the result is published or unpublished changes.
|
|
92
94
|
`;
|
|
95
|
+
const LUMINE_REFERENCE_INSTRUCTIONS = `${LUMINE_REFERENCE_INSTRUCTIONS_MARKER}
|
|
96
|
+
# Lumine Reference Guide
|
|
97
|
+
|
|
98
|
+
This directory contains read-only reference files pulled from a public
|
|
99
|
+
open-source Twinkle Build. Use it for inspection and borrowing patterns, not as
|
|
100
|
+
the workspace to save.
|
|
101
|
+
|
|
102
|
+
## Source Of Truth
|
|
103
|
+
|
|
104
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
105
|
+
- If metadata.readOnly is true or build.role is "reference", do not run lumine save from this directory.
|
|
106
|
+
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
107
|
+
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
108
|
+
`;
|
|
93
109
|
const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
94
110
|
const COMMANDS = new Set([
|
|
95
111
|
"workspace",
|
|
@@ -97,8 +113,11 @@ const COMMANDS = new Set([
|
|
|
97
113
|
"logout",
|
|
98
114
|
"whoami",
|
|
99
115
|
"projects",
|
|
116
|
+
"explore",
|
|
100
117
|
"select",
|
|
101
118
|
"pull",
|
|
119
|
+
"reference",
|
|
120
|
+
"fork",
|
|
102
121
|
"save",
|
|
103
122
|
"push",
|
|
104
123
|
"check",
|
|
@@ -138,6 +157,10 @@ async function main() {
|
|
|
138
157
|
await projects(options);
|
|
139
158
|
return;
|
|
140
159
|
}
|
|
160
|
+
if (options.command === "explore") {
|
|
161
|
+
await explore(options);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
141
164
|
if (options.command === "select") {
|
|
142
165
|
await selectProject(options);
|
|
143
166
|
return;
|
|
@@ -146,6 +169,14 @@ async function main() {
|
|
|
146
169
|
await pull(options);
|
|
147
170
|
return;
|
|
148
171
|
}
|
|
172
|
+
if (options.command === "reference") {
|
|
173
|
+
await reference(options);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (options.command === "fork") {
|
|
177
|
+
await fork(options);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
149
180
|
if (options.command === "save" || options.command === "push") {
|
|
150
181
|
await save(options);
|
|
151
182
|
return;
|
|
@@ -286,6 +317,12 @@ async function projects(options) {
|
|
|
286
317
|
printBuildList(builds);
|
|
287
318
|
}
|
|
288
319
|
|
|
320
|
+
async function explore(options) {
|
|
321
|
+
const auth = await resolveAuth(options);
|
|
322
|
+
const builds = await listOpenSourceBuilds({ options, auth });
|
|
323
|
+
printOpenSourceBuildList(builds, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
289
326
|
async function selectProject(options) {
|
|
290
327
|
const auth = await resolveAuth(options);
|
|
291
328
|
const selectedBuild = options.target
|
|
@@ -326,12 +363,38 @@ async function pull(options) {
|
|
|
326
363
|
printPullResult(result);
|
|
327
364
|
}
|
|
328
365
|
|
|
366
|
+
async function reference(options) {
|
|
367
|
+
const auth = await resolveAuth(options);
|
|
368
|
+
const buildId = resolveRequiredBuildId(options.target);
|
|
369
|
+
const result = await pullReferenceFiles({ options, auth, buildId });
|
|
370
|
+
printReferenceResult(result);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function fork(options) {
|
|
374
|
+
const auth = await resolveAuth(options);
|
|
375
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
376
|
+
const buildId = resolveRequiredBuildId(options.target);
|
|
377
|
+
const forkResult = await forkBuild({ options, auth, buildId });
|
|
378
|
+
const forkedBuildId = Number(forkResult.build?.id || 0);
|
|
379
|
+
if (!forkedBuildId) {
|
|
380
|
+
throw new Error("Twinkle did not return a forked Build.");
|
|
381
|
+
}
|
|
382
|
+
const result = await pullBuildFiles({
|
|
383
|
+
options,
|
|
384
|
+
auth,
|
|
385
|
+
buildId: forkedBuildId,
|
|
386
|
+
});
|
|
387
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
388
|
+
printForkResult({ forkResult, pullResult: result });
|
|
389
|
+
}
|
|
390
|
+
|
|
329
391
|
async function save(options) {
|
|
330
392
|
const auth = await resolveAuth(options);
|
|
331
393
|
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
332
394
|
const localProject = await findLocalProjectMetadata(
|
|
333
395
|
path.resolve(options.dir || process.cwd()),
|
|
334
396
|
);
|
|
397
|
+
assertLocalProjectCanBeSaved(localProject);
|
|
335
398
|
let buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
336
399
|
localProject,
|
|
337
400
|
});
|
|
@@ -543,6 +606,21 @@ async function listBuilds({ options, auth }) {
|
|
|
543
606
|
return Array.isArray(result.builds) ? result.builds : [];
|
|
544
607
|
}
|
|
545
608
|
|
|
609
|
+
async function listOpenSourceBuilds({ options, auth }) {
|
|
610
|
+
const url = new URL(`${options.apiUrl}/cli/open-source-builds`);
|
|
611
|
+
url.searchParams.set("limit", String(options.limit));
|
|
612
|
+
url.searchParams.set("sort", options.sort);
|
|
613
|
+
if (options.searchQuery) {
|
|
614
|
+
url.searchParams.set("search", options.searchQuery);
|
|
615
|
+
}
|
|
616
|
+
const result = await requestJson({
|
|
617
|
+
url: url.toString(),
|
|
618
|
+
authToken: auth.token,
|
|
619
|
+
timeoutMs: options.timeoutMs,
|
|
620
|
+
});
|
|
621
|
+
return Array.isArray(result.builds) ? result.builds : [];
|
|
622
|
+
}
|
|
623
|
+
|
|
546
624
|
async function loadBuildMetadata({ options, auth, buildId }) {
|
|
547
625
|
const result = await loadBuildFiles({
|
|
548
626
|
options,
|
|
@@ -556,6 +634,21 @@ async function loadBuildMetadata({ options, auth, buildId }) {
|
|
|
556
634
|
return result.build;
|
|
557
635
|
}
|
|
558
636
|
|
|
637
|
+
async function loadOpenSourceBuildFiles({
|
|
638
|
+
options,
|
|
639
|
+
auth,
|
|
640
|
+
buildId,
|
|
641
|
+
includeContent,
|
|
642
|
+
}) {
|
|
643
|
+
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/open-source-files`);
|
|
644
|
+
if (!includeContent) url.searchParams.set("includeContent", "0");
|
|
645
|
+
return await requestJson({
|
|
646
|
+
url: url.toString(),
|
|
647
|
+
authToken: auth.token,
|
|
648
|
+
timeoutMs: options.timeoutMs,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
559
652
|
async function loadBuildFiles({ options, auth, buildId, includeContent }) {
|
|
560
653
|
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/files`);
|
|
561
654
|
if (!includeContent) url.searchParams.set("includeContent", "0");
|
|
@@ -566,6 +659,16 @@ async function loadBuildFiles({ options, auth, buildId, includeContent }) {
|
|
|
566
659
|
});
|
|
567
660
|
}
|
|
568
661
|
|
|
662
|
+
async function forkBuild({ options, auth, buildId }) {
|
|
663
|
+
return await requestJson({
|
|
664
|
+
method: "POST",
|
|
665
|
+
url: `${options.apiUrl}/build/${buildId}/fork`,
|
|
666
|
+
authToken: auth.token,
|
|
667
|
+
body: {},
|
|
668
|
+
timeoutMs: options.timeoutMs,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
569
672
|
async function saveProjectFiles({ options, auth, buildId, files, summary }) {
|
|
570
673
|
return await requestJson({
|
|
571
674
|
method: "PUT",
|
|
@@ -754,6 +857,40 @@ async function pullBuildFiles({ options, auth, buildId }) {
|
|
|
754
857
|
};
|
|
755
858
|
}
|
|
756
859
|
|
|
860
|
+
async function pullReferenceFiles({ options, auth, buildId }) {
|
|
861
|
+
const result = await loadOpenSourceBuildFiles({
|
|
862
|
+
options,
|
|
863
|
+
auth,
|
|
864
|
+
buildId,
|
|
865
|
+
includeContent: true,
|
|
866
|
+
});
|
|
867
|
+
const build = result.build || { id: buildId, title: `Build ${buildId}` };
|
|
868
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
869
|
+
const dir = path.resolve(options.dir || defaultReferenceDir(build));
|
|
870
|
+
await writeProjectFiles({ dir, files });
|
|
871
|
+
await writeReferenceInstructions({ dir });
|
|
872
|
+
await writeSdkReference({ dir });
|
|
873
|
+
await writeReferenceMetadata({
|
|
874
|
+
dir,
|
|
875
|
+
options,
|
|
876
|
+
build,
|
|
877
|
+
manifest: result.projectManifest || null,
|
|
878
|
+
reference: result.reference || {
|
|
879
|
+
readOnly: true,
|
|
880
|
+
forkable: true,
|
|
881
|
+
sourceBuildId: Number(build.id || buildId),
|
|
882
|
+
},
|
|
883
|
+
pulledAt: new Date().toISOString(),
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
build,
|
|
887
|
+
dir,
|
|
888
|
+
fileCount: files.length,
|
|
889
|
+
manifest: result.projectManifest || null,
|
|
890
|
+
reference: result.reference || null,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
757
894
|
async function writeAgentInstructions({ dir }) {
|
|
758
895
|
for (const fileName of AGENT_INSTRUCTION_FILES) {
|
|
759
896
|
const filePath = path.join(dir, fileName);
|
|
@@ -769,6 +906,21 @@ async function writeAgentInstructions({ dir }) {
|
|
|
769
906
|
}
|
|
770
907
|
}
|
|
771
908
|
|
|
909
|
+
async function writeReferenceInstructions({ dir }) {
|
|
910
|
+
for (const fileName of AGENT_INSTRUCTION_FILES) {
|
|
911
|
+
const filePath = path.join(dir, fileName);
|
|
912
|
+
try {
|
|
913
|
+
const existing = await fs.readFile(filePath, "utf8");
|
|
914
|
+
if (!existing.includes(LUMINE_REFERENCE_INSTRUCTIONS_MARKER)) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error.code !== "ENOENT") throw error;
|
|
919
|
+
}
|
|
920
|
+
await fs.writeFile(filePath, LUMINE_REFERENCE_INSTRUCTIONS, "utf8");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
772
924
|
async function writeSdkReference({ dir }) {
|
|
773
925
|
const filePath = path.join(dir, SDK_REFERENCE_FILE);
|
|
774
926
|
try {
|
|
@@ -976,6 +1128,52 @@ async function writeProjectMetadata({
|
|
|
976
1128
|
);
|
|
977
1129
|
}
|
|
978
1130
|
|
|
1131
|
+
async function writeReferenceMetadata({
|
|
1132
|
+
dir,
|
|
1133
|
+
options,
|
|
1134
|
+
build,
|
|
1135
|
+
manifest,
|
|
1136
|
+
reference,
|
|
1137
|
+
pulledAt,
|
|
1138
|
+
}) {
|
|
1139
|
+
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
1140
|
+
await fs.mkdir(metadataDir, { recursive: true });
|
|
1141
|
+
const sourceBuildId =
|
|
1142
|
+
Number(reference?.sourceBuildId || 0) || Number(build?.id || 0) || null;
|
|
1143
|
+
await fs.writeFile(
|
|
1144
|
+
path.join(metadataDir, PROJECT_METADATA_FILE),
|
|
1145
|
+
JSON.stringify(
|
|
1146
|
+
{
|
|
1147
|
+
schemaVersion: 1,
|
|
1148
|
+
buildId: sourceBuildId,
|
|
1149
|
+
readOnly: true,
|
|
1150
|
+
reference: {
|
|
1151
|
+
readOnly: true,
|
|
1152
|
+
forkable: true,
|
|
1153
|
+
sourceBuildId,
|
|
1154
|
+
sourceAppUrl: sourceBuildId ? `${options.siteUrl}/app/${sourceBuildId}` : null,
|
|
1155
|
+
},
|
|
1156
|
+
build: {
|
|
1157
|
+
id: sourceBuildId,
|
|
1158
|
+
title: build?.title || (sourceBuildId ? `Build ${sourceBuildId}` : ""),
|
|
1159
|
+
role: "reference",
|
|
1160
|
+
ownerUsername: build?.ownerUsername || null,
|
|
1161
|
+
collaborationMode: build?.collaborationMode || "open_source",
|
|
1162
|
+
canWrite: false,
|
|
1163
|
+
canPublish: false,
|
|
1164
|
+
},
|
|
1165
|
+
apiUrl: options.apiUrl,
|
|
1166
|
+
siteUrl: options.siteUrl,
|
|
1167
|
+
manifest,
|
|
1168
|
+
pulledAt,
|
|
1169
|
+
},
|
|
1170
|
+
null,
|
|
1171
|
+
2,
|
|
1172
|
+
),
|
|
1173
|
+
"utf8",
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
979
1177
|
async function findLocalProjectMetadata(startDir) {
|
|
980
1178
|
let current = path.resolve(startDir || process.cwd());
|
|
981
1179
|
while (true) {
|
|
@@ -1002,6 +1200,29 @@ function resolveProjectDirForSave({ options, localProject }) {
|
|
|
1002
1200
|
return process.cwd();
|
|
1003
1201
|
}
|
|
1004
1202
|
|
|
1203
|
+
function assertLocalProjectCanBeSaved(localProject) {
|
|
1204
|
+
const metadata = localProject?.metadata;
|
|
1205
|
+
if (!metadata) return;
|
|
1206
|
+
if (isReadOnlyReferenceMetadata(metadata)) {
|
|
1207
|
+
const sourceBuildId =
|
|
1208
|
+
Number(metadata.reference?.sourceBuildId || 0) ||
|
|
1209
|
+
Number(metadata.buildId || 0) ||
|
|
1210
|
+
Number(metadata.build?.id || 0) ||
|
|
1211
|
+
0;
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function isReadOnlyReferenceMetadata(metadata) {
|
|
1219
|
+
return (
|
|
1220
|
+
metadata?.readOnly === true ||
|
|
1221
|
+
metadata?.reference?.readOnly === true ||
|
|
1222
|
+
metadata?.build?.role === "reference"
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1005
1226
|
function resolveLocalProjectFilePath({ rootDir, projectPath }) {
|
|
1006
1227
|
const relativePath = projectPathToRelativePath(projectPath);
|
|
1007
1228
|
const root = path.resolve(rootDir);
|
|
@@ -1048,6 +1269,23 @@ function printBuildList(builds) {
|
|
|
1048
1269
|
});
|
|
1049
1270
|
}
|
|
1050
1271
|
|
|
1272
|
+
function printOpenSourceBuildList(builds, options) {
|
|
1273
|
+
if (!builds.length) {
|
|
1274
|
+
const searchText = options.searchQuery
|
|
1275
|
+
? ` matching "${options.searchQuery}"`
|
|
1276
|
+
: "";
|
|
1277
|
+
console.log(`No public open-source Twinkle builds found${searchText}.`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const searchText = options.searchQuery ? ` for "${options.searchQuery}"` : "";
|
|
1281
|
+
console.log(`Public open-source Twinkle builds${searchText}:`);
|
|
1282
|
+
builds.forEach((build, index) => {
|
|
1283
|
+
console.log(`${index + 1}. ${formatOpenSourceBuildListItem(build)}`);
|
|
1284
|
+
});
|
|
1285
|
+
console.log("Reference: lumine reference <build-id>");
|
|
1286
|
+
console.log("Fork: lumine fork <build-id>");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1051
1289
|
function formatBuildListItem(build) {
|
|
1052
1290
|
const role =
|
|
1053
1291
|
build.role === "owner"
|
|
@@ -1057,6 +1295,16 @@ function formatBuildListItem(build) {
|
|
|
1057
1295
|
return `${formatBuildTitle(build)} - ${role}, ${published}`;
|
|
1058
1296
|
}
|
|
1059
1297
|
|
|
1298
|
+
function formatOpenSourceBuildListItem(build) {
|
|
1299
|
+
const owner = build.ownerUsername ? ` by ${build.ownerUsername}` : "";
|
|
1300
|
+
const stats = [
|
|
1301
|
+
`${Math.max(0, Number(build.forkCount || 0))} forks`,
|
|
1302
|
+
`${Math.max(0, Number(build.viewCount || 0))} views`,
|
|
1303
|
+
].join(", ");
|
|
1304
|
+
const appUrl = build.appUrl ? ` - ${build.appUrl}` : "";
|
|
1305
|
+
return `${formatBuildTitle(build)}${owner} - ${stats}${appUrl}`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1060
1308
|
function formatBuildTitle(build) {
|
|
1061
1309
|
return `${build.title || `Build ${build.id}`} (#${build.id})`;
|
|
1062
1310
|
}
|
|
@@ -1095,6 +1343,39 @@ function printPullResult(result) {
|
|
|
1095
1343
|
}
|
|
1096
1344
|
}
|
|
1097
1345
|
|
|
1346
|
+
function printReferenceResult(result) {
|
|
1347
|
+
const build = result.build || {};
|
|
1348
|
+
const sourceBuildId =
|
|
1349
|
+
Number(result.reference?.sourceBuildId || 0) || Number(build.id || 0);
|
|
1350
|
+
const entryPath = result.manifest?.entryPath || "unknown";
|
|
1351
|
+
console.log(`Referenced ${formatBuildTitle(build)}.`);
|
|
1352
|
+
console.log(
|
|
1353
|
+
`Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
|
|
1354
|
+
);
|
|
1355
|
+
console.log(`Entry: ${entryPath}`);
|
|
1356
|
+
console.log("Mode: read-only reference");
|
|
1357
|
+
console.log(`Next: cd ${shellQuote(result.dir)}`);
|
|
1358
|
+
if (sourceBuildId) {
|
|
1359
|
+
console.log(`Start from this app: lumine fork ${sourceBuildId}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function printForkResult({ forkResult, pullResult }) {
|
|
1364
|
+
const build = pullResult.build || forkResult.build || {};
|
|
1365
|
+
const sourceBuildId =
|
|
1366
|
+
Number(forkResult.sourceBuild?.id || 0) ||
|
|
1367
|
+
Number(forkResult.sourceBuild?.contentId || 0) ||
|
|
1368
|
+
0;
|
|
1369
|
+
console.log(
|
|
1370
|
+
forkResult.alreadyExists
|
|
1371
|
+
? `Using your existing fork ${formatBuildTitle(build)}.`
|
|
1372
|
+
: `Forked Build ${sourceBuildId || "source"} into ${formatBuildTitle(
|
|
1373
|
+
build,
|
|
1374
|
+
)}.`,
|
|
1375
|
+
);
|
|
1376
|
+
printPullResult(pullResult);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1098
1379
|
function printSaveResult({ result, build, dir, files }) {
|
|
1099
1380
|
const entryPath = result.projectManifest?.entryPath || "unknown";
|
|
1100
1381
|
const version = result.artifactVersion?.versionNumber
|
|
@@ -1263,6 +1544,13 @@ function parseArgs(args) {
|
|
|
1263
1544
|
return {
|
|
1264
1545
|
command,
|
|
1265
1546
|
target: raw.url || raw.target || positional[0] || "",
|
|
1547
|
+
searchQuery:
|
|
1548
|
+
String(
|
|
1549
|
+
raw.search ||
|
|
1550
|
+
raw.query ||
|
|
1551
|
+
(command === "explore" ? positional.join(" ") : ""),
|
|
1552
|
+
).trim() || "",
|
|
1553
|
+
sort: normalizeOpenSourceSort(raw.sort),
|
|
1266
1554
|
apiUrl: trimTrailingSlash(
|
|
1267
1555
|
String(raw.apiUrl || process.env.TWINKLE_API_URL || DEFAULT_API_URL),
|
|
1268
1556
|
),
|
|
@@ -1312,6 +1600,19 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
1312
1600
|
(await findLocalProjectMetadata(
|
|
1313
1601
|
path.resolve(options.dir || process.cwd()),
|
|
1314
1602
|
));
|
|
1603
|
+
if (
|
|
1604
|
+
resolvedLocalProject?.metadata &&
|
|
1605
|
+
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
1606
|
+
) {
|
|
1607
|
+
const sourceBuildId =
|
|
1608
|
+
Number(resolvedLocalProject.metadata.reference?.sourceBuildId || 0) ||
|
|
1609
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1610
|
+
Number(resolvedLocalProject.metadata.build?.id || 0) ||
|
|
1611
|
+
0;
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace, or pass an explicit Build URL.`,
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1315
1616
|
const localBuildId = Number(resolvedLocalProject?.metadata?.buildId || 0);
|
|
1316
1617
|
if (localBuildId > 0) return localBuildId;
|
|
1317
1618
|
const selectedBuildId = Number(auth?.selectedBuildId || 0);
|
|
@@ -1403,6 +1704,14 @@ function parseBoolean(value, fallback) {
|
|
|
1403
1704
|
return fallback;
|
|
1404
1705
|
}
|
|
1405
1706
|
|
|
1707
|
+
function normalizeOpenSourceSort(value) {
|
|
1708
|
+
const normalized = String(value || "")
|
|
1709
|
+
.trim()
|
|
1710
|
+
.toLowerCase();
|
|
1711
|
+
if (["recent", "popular", "forks"].includes(normalized)) return normalized;
|
|
1712
|
+
return "forks";
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1406
1715
|
async function openBrowser(url) {
|
|
1407
1716
|
const command =
|
|
1408
1717
|
process.platform === "darwin"
|
|
@@ -1435,6 +1744,12 @@ function defaultWorkspaceDir(build) {
|
|
|
1435
1744
|
return `twinkle-${titleSlug || "build"}-${buildId}`;
|
|
1436
1745
|
}
|
|
1437
1746
|
|
|
1747
|
+
function defaultReferenceDir(build) {
|
|
1748
|
+
const titleSlug = slugify(build?.title || "");
|
|
1749
|
+
const buildId = Number(build?.id || 0) || "build";
|
|
1750
|
+
return `twinkle-reference-${titleSlug || "build"}-${buildId}`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1438
1753
|
function slugify(value) {
|
|
1439
1754
|
return String(value || "")
|
|
1440
1755
|
.toLowerCase()
|
|
@@ -1454,8 +1769,11 @@ function printHelp() {
|
|
|
1454
1769
|
lumine whoami
|
|
1455
1770
|
lumine logout
|
|
1456
1771
|
lumine projects
|
|
1772
|
+
lumine explore [search terms]
|
|
1457
1773
|
lumine select [twinkle-build-url]
|
|
1458
1774
|
lumine pull [twinkle-build-url]
|
|
1775
|
+
lumine reference <twinkle-build-url>
|
|
1776
|
+
lumine fork <twinkle-build-url>
|
|
1459
1777
|
lumine save
|
|
1460
1778
|
lumine check [twinkle-build-url]
|
|
1461
1779
|
lumine launch [twinkle-build-url]
|
|
@@ -1463,6 +1781,9 @@ function printHelp() {
|
|
|
1463
1781
|
Examples:
|
|
1464
1782
|
npx @stage5/lumine@latest
|
|
1465
1783
|
npx @stage5/lumine@latest login
|
|
1784
|
+
npx @stage5/lumine@latest explore --sort forks
|
|
1785
|
+
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
1786
|
+
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
1466
1787
|
npx @stage5/lumine@latest pull
|
|
1467
1788
|
npx @stage5/lumine@latest save
|
|
1468
1789
|
npx @stage5/lumine@latest save --publish
|
|
@@ -1476,6 +1797,8 @@ Options:
|
|
|
1476
1797
|
--auth-token <token> Override saved login
|
|
1477
1798
|
--dir <path> Directory for pulled project files
|
|
1478
1799
|
--summary <text> Save summary
|
|
1800
|
+
--search <text> Search public open-source Builds
|
|
1801
|
+
--sort <sort> Sort open-source Builds: forks, popular, recent
|
|
1479
1802
|
--publish Publish after saving
|
|
1480
1803
|
--save Save local files before launch
|
|
1481
1804
|
--limit <number> Number of projects to show
|