@stage5/lumine 0.1.3 → 0.1.5
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 +20 -0
- package/bin/lumine.js +475 -1
- 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,16 @@ 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
|
+
|
|
52
|
+
The CLI checks npm for the latest `@stage5/lumine` version on normal commands.
|
|
53
|
+
If the installed copy is outdated, it prints an update warning and records the
|
|
54
|
+
version state in `.twinkle/lumine-project.json` so local agents can tell when
|
|
55
|
+
they should rerun with `npx @stage5/lumine@latest`. Use `--no-update-check` to
|
|
56
|
+
skip that advisory network check.
|
|
57
|
+
|
|
38
58
|
After pulling a project, run an agent from the pulled folder:
|
|
39
59
|
|
|
40
60
|
```bash
|
package/bin/lumine.js
CHANGED
|
@@ -8,12 +8,14 @@ import { stdin as input, stdout as output } from "process";
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_API_URL = "https://api.twinkle.network";
|
|
10
10
|
const DEFAULT_SITE_URL = "https://www.twin-kle.com";
|
|
11
|
+
const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
11
12
|
const DEFAULT_AUTH_FILE = path.join(
|
|
12
13
|
os.homedir(),
|
|
13
14
|
".twinkle",
|
|
14
15
|
"lumine-cli-auth.json",
|
|
15
16
|
);
|
|
16
17
|
const DEFAULT_TIMEOUT_MS = 20000;
|
|
18
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
17
19
|
const DEFAULT_PROJECT_LIMIT = 50;
|
|
18
20
|
const PROJECT_METADATA_DIR = ".twinkle";
|
|
19
21
|
const PROJECT_METADATA_FILE = "lumine-project.json";
|
|
@@ -29,10 +31,13 @@ const EXCLUDED_UPLOAD_FILES = new Set([
|
|
|
29
31
|
const LUMINE_AGENT_INSTRUCTIONS_MARKER =
|
|
30
32
|
"<!-- Lumine CLI Agent Instructions -->";
|
|
31
33
|
const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
|
|
34
|
+
const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
35
|
+
"<!-- Lumine CLI Reference Instructions -->";
|
|
32
36
|
const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
33
37
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
34
38
|
import.meta.url,
|
|
35
39
|
);
|
|
40
|
+
const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
|
|
36
41
|
const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
|
|
37
42
|
# Twinkle Build SDK Reference
|
|
38
43
|
|
|
@@ -59,6 +64,7 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
|
59
64
|
|
|
60
65
|
- Read .twinkle/lumine-project.json before changing files.
|
|
61
66
|
- Treat build.canWrite, build.canPublish, and build.contributionRootBuildId as authoritative.
|
|
67
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before saving.
|
|
62
68
|
- Read ${SDK_REFERENCE_FILE} before adding, removing, or changing any Twinkle.* SDK calls.
|
|
63
69
|
- If build.canWrite is false, do not save changes.
|
|
64
70
|
- If build.canPublish is false or contributionRootBuildId is set, this checkout is a contribution branch. Save only to this branch and do not run lumine launch or lumine save --publish.
|
|
@@ -90,6 +96,21 @@ lumine save --summary "Describe the change"
|
|
|
90
96
|
Report the changed files, any SDK methods used, the lumine save result, the
|
|
91
97
|
build or branch id, and whether the result is published or unpublished changes.
|
|
92
98
|
`;
|
|
99
|
+
const LUMINE_REFERENCE_INSTRUCTIONS = `${LUMINE_REFERENCE_INSTRUCTIONS_MARKER}
|
|
100
|
+
# Lumine Reference Guide
|
|
101
|
+
|
|
102
|
+
This directory contains read-only reference files pulled from a public
|
|
103
|
+
open-source Twinkle Build. Use it for inspection and borrowing patterns, not as
|
|
104
|
+
the workspace to save.
|
|
105
|
+
|
|
106
|
+
## Source Of Truth
|
|
107
|
+
|
|
108
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
109
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before borrowing patterns.
|
|
110
|
+
- If metadata.readOnly is true or build.role is "reference", do not run lumine save from this directory.
|
|
111
|
+
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
112
|
+
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
113
|
+
`;
|
|
93
114
|
const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
94
115
|
const COMMANDS = new Set([
|
|
95
116
|
"workspace",
|
|
@@ -97,8 +118,11 @@ const COMMANDS = new Set([
|
|
|
97
118
|
"logout",
|
|
98
119
|
"whoami",
|
|
99
120
|
"projects",
|
|
121
|
+
"explore",
|
|
100
122
|
"select",
|
|
101
123
|
"pull",
|
|
124
|
+
"reference",
|
|
125
|
+
"fork",
|
|
102
126
|
"save",
|
|
103
127
|
"push",
|
|
104
128
|
"check",
|
|
@@ -113,10 +137,14 @@ main().catch((error) => {
|
|
|
113
137
|
|
|
114
138
|
async function main() {
|
|
115
139
|
const options = parseArgs(process.argv.slice(2));
|
|
140
|
+
options.lumineCli = await loadLumineCliVersionInfo({ options });
|
|
116
141
|
if (options.help) {
|
|
117
142
|
printHelp();
|
|
118
143
|
return;
|
|
119
144
|
}
|
|
145
|
+
if (options.updateCheck) {
|
|
146
|
+
await maybeCheckForLumineCliUpdate({ options });
|
|
147
|
+
}
|
|
120
148
|
|
|
121
149
|
if (options.command === "workspace") {
|
|
122
150
|
await workspace(options);
|
|
@@ -138,6 +166,10 @@ async function main() {
|
|
|
138
166
|
await projects(options);
|
|
139
167
|
return;
|
|
140
168
|
}
|
|
169
|
+
if (options.command === "explore") {
|
|
170
|
+
await explore(options);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
141
173
|
if (options.command === "select") {
|
|
142
174
|
await selectProject(options);
|
|
143
175
|
return;
|
|
@@ -146,6 +178,14 @@ async function main() {
|
|
|
146
178
|
await pull(options);
|
|
147
179
|
return;
|
|
148
180
|
}
|
|
181
|
+
if (options.command === "reference") {
|
|
182
|
+
await reference(options);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (options.command === "fork") {
|
|
186
|
+
await fork(options);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
149
189
|
if (options.command === "save" || options.command === "push") {
|
|
150
190
|
await save(options);
|
|
151
191
|
return;
|
|
@@ -286,6 +326,12 @@ async function projects(options) {
|
|
|
286
326
|
printBuildList(builds);
|
|
287
327
|
}
|
|
288
328
|
|
|
329
|
+
async function explore(options) {
|
|
330
|
+
const auth = await resolveAuth(options);
|
|
331
|
+
const builds = await listOpenSourceBuilds({ options, auth });
|
|
332
|
+
printOpenSourceBuildList(builds, options);
|
|
333
|
+
}
|
|
334
|
+
|
|
289
335
|
async function selectProject(options) {
|
|
290
336
|
const auth = await resolveAuth(options);
|
|
291
337
|
const selectedBuild = options.target
|
|
@@ -326,12 +372,38 @@ async function pull(options) {
|
|
|
326
372
|
printPullResult(result);
|
|
327
373
|
}
|
|
328
374
|
|
|
375
|
+
async function reference(options) {
|
|
376
|
+
const auth = await resolveAuth(options);
|
|
377
|
+
const buildId = resolveRequiredBuildId(options.target);
|
|
378
|
+
const result = await pullReferenceFiles({ options, auth, buildId });
|
|
379
|
+
printReferenceResult(result);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function fork(options) {
|
|
383
|
+
const auth = await resolveAuth(options);
|
|
384
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
385
|
+
const buildId = resolveRequiredBuildId(options.target);
|
|
386
|
+
const forkResult = await forkBuild({ options, auth, buildId });
|
|
387
|
+
const forkedBuildId = Number(forkResult.build?.id || 0);
|
|
388
|
+
if (!forkedBuildId) {
|
|
389
|
+
throw new Error("Twinkle did not return a forked Build.");
|
|
390
|
+
}
|
|
391
|
+
const result = await pullBuildFiles({
|
|
392
|
+
options,
|
|
393
|
+
auth,
|
|
394
|
+
buildId: forkedBuildId,
|
|
395
|
+
});
|
|
396
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
397
|
+
printForkResult({ forkResult, pullResult: result });
|
|
398
|
+
}
|
|
399
|
+
|
|
329
400
|
async function save(options) {
|
|
330
401
|
const auth = await resolveAuth(options);
|
|
331
402
|
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
332
403
|
const localProject = await findLocalProjectMetadata(
|
|
333
404
|
path.resolve(options.dir || process.cwd()),
|
|
334
405
|
);
|
|
406
|
+
assertLocalProjectCanBeSaved(localProject);
|
|
335
407
|
let buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
336
408
|
localProject,
|
|
337
409
|
});
|
|
@@ -543,6 +615,21 @@ async function listBuilds({ options, auth }) {
|
|
|
543
615
|
return Array.isArray(result.builds) ? result.builds : [];
|
|
544
616
|
}
|
|
545
617
|
|
|
618
|
+
async function listOpenSourceBuilds({ options, auth }) {
|
|
619
|
+
const url = new URL(`${options.apiUrl}/cli/open-source-builds`);
|
|
620
|
+
url.searchParams.set("limit", String(options.limit));
|
|
621
|
+
url.searchParams.set("sort", options.sort);
|
|
622
|
+
if (options.searchQuery) {
|
|
623
|
+
url.searchParams.set("search", options.searchQuery);
|
|
624
|
+
}
|
|
625
|
+
const result = await requestJson({
|
|
626
|
+
url: url.toString(),
|
|
627
|
+
authToken: auth.token,
|
|
628
|
+
timeoutMs: options.timeoutMs,
|
|
629
|
+
});
|
|
630
|
+
return Array.isArray(result.builds) ? result.builds : [];
|
|
631
|
+
}
|
|
632
|
+
|
|
546
633
|
async function loadBuildMetadata({ options, auth, buildId }) {
|
|
547
634
|
const result = await loadBuildFiles({
|
|
548
635
|
options,
|
|
@@ -556,6 +643,21 @@ async function loadBuildMetadata({ options, auth, buildId }) {
|
|
|
556
643
|
return result.build;
|
|
557
644
|
}
|
|
558
645
|
|
|
646
|
+
async function loadOpenSourceBuildFiles({
|
|
647
|
+
options,
|
|
648
|
+
auth,
|
|
649
|
+
buildId,
|
|
650
|
+
includeContent,
|
|
651
|
+
}) {
|
|
652
|
+
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/open-source-files`);
|
|
653
|
+
if (!includeContent) url.searchParams.set("includeContent", "0");
|
|
654
|
+
return await requestJson({
|
|
655
|
+
url: url.toString(),
|
|
656
|
+
authToken: auth.token,
|
|
657
|
+
timeoutMs: options.timeoutMs,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
559
661
|
async function loadBuildFiles({ options, auth, buildId, includeContent }) {
|
|
560
662
|
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/files`);
|
|
561
663
|
if (!includeContent) url.searchParams.set("includeContent", "0");
|
|
@@ -566,6 +668,16 @@ async function loadBuildFiles({ options, auth, buildId, includeContent }) {
|
|
|
566
668
|
});
|
|
567
669
|
}
|
|
568
670
|
|
|
671
|
+
async function forkBuild({ options, auth, buildId }) {
|
|
672
|
+
return await requestJson({
|
|
673
|
+
method: "POST",
|
|
674
|
+
url: `${options.apiUrl}/build/${buildId}/fork`,
|
|
675
|
+
authToken: auth.token,
|
|
676
|
+
body: {},
|
|
677
|
+
timeoutMs: options.timeoutMs,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
569
681
|
async function saveProjectFiles({ options, auth, buildId, files, summary }) {
|
|
570
682
|
return await requestJson({
|
|
571
683
|
method: "PUT",
|
|
@@ -754,6 +866,40 @@ async function pullBuildFiles({ options, auth, buildId }) {
|
|
|
754
866
|
};
|
|
755
867
|
}
|
|
756
868
|
|
|
869
|
+
async function pullReferenceFiles({ options, auth, buildId }) {
|
|
870
|
+
const result = await loadOpenSourceBuildFiles({
|
|
871
|
+
options,
|
|
872
|
+
auth,
|
|
873
|
+
buildId,
|
|
874
|
+
includeContent: true,
|
|
875
|
+
});
|
|
876
|
+
const build = result.build || { id: buildId, title: `Build ${buildId}` };
|
|
877
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
878
|
+
const dir = path.resolve(options.dir || defaultReferenceDir(build));
|
|
879
|
+
await writeProjectFiles({ dir, files });
|
|
880
|
+
await writeReferenceInstructions({ dir });
|
|
881
|
+
await writeSdkReference({ dir });
|
|
882
|
+
await writeReferenceMetadata({
|
|
883
|
+
dir,
|
|
884
|
+
options,
|
|
885
|
+
build,
|
|
886
|
+
manifest: result.projectManifest || null,
|
|
887
|
+
reference: result.reference || {
|
|
888
|
+
readOnly: true,
|
|
889
|
+
forkable: true,
|
|
890
|
+
sourceBuildId: Number(build.id || buildId),
|
|
891
|
+
},
|
|
892
|
+
pulledAt: new Date().toISOString(),
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
build,
|
|
896
|
+
dir,
|
|
897
|
+
fileCount: files.length,
|
|
898
|
+
manifest: result.projectManifest || null,
|
|
899
|
+
reference: result.reference || null,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
757
903
|
async function writeAgentInstructions({ dir }) {
|
|
758
904
|
for (const fileName of AGENT_INSTRUCTION_FILES) {
|
|
759
905
|
const filePath = path.join(dir, fileName);
|
|
@@ -769,6 +915,21 @@ async function writeAgentInstructions({ dir }) {
|
|
|
769
915
|
}
|
|
770
916
|
}
|
|
771
917
|
|
|
918
|
+
async function writeReferenceInstructions({ dir }) {
|
|
919
|
+
for (const fileName of AGENT_INSTRUCTION_FILES) {
|
|
920
|
+
const filePath = path.join(dir, fileName);
|
|
921
|
+
try {
|
|
922
|
+
const existing = await fs.readFile(filePath, "utf8");
|
|
923
|
+
if (!existing.includes(LUMINE_REFERENCE_INSTRUCTIONS_MARKER)) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (error.code !== "ENOENT") throw error;
|
|
928
|
+
}
|
|
929
|
+
await fs.writeFile(filePath, LUMINE_REFERENCE_INSTRUCTIONS, "utf8");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
772
933
|
async function writeSdkReference({ dir }) {
|
|
773
934
|
const filePath = path.join(dir, SDK_REFERENCE_FILE);
|
|
774
935
|
try {
|
|
@@ -965,6 +1126,7 @@ async function writeProjectMetadata({
|
|
|
965
1126
|
: null,
|
|
966
1127
|
apiUrl: options.apiUrl,
|
|
967
1128
|
siteUrl: options.siteUrl,
|
|
1129
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
968
1130
|
manifest,
|
|
969
1131
|
pulledAt,
|
|
970
1132
|
lastSavedAt,
|
|
@@ -976,6 +1138,53 @@ async function writeProjectMetadata({
|
|
|
976
1138
|
);
|
|
977
1139
|
}
|
|
978
1140
|
|
|
1141
|
+
async function writeReferenceMetadata({
|
|
1142
|
+
dir,
|
|
1143
|
+
options,
|
|
1144
|
+
build,
|
|
1145
|
+
manifest,
|
|
1146
|
+
reference,
|
|
1147
|
+
pulledAt,
|
|
1148
|
+
}) {
|
|
1149
|
+
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
1150
|
+
await fs.mkdir(metadataDir, { recursive: true });
|
|
1151
|
+
const sourceBuildId =
|
|
1152
|
+
Number(reference?.sourceBuildId || 0) || Number(build?.id || 0) || null;
|
|
1153
|
+
await fs.writeFile(
|
|
1154
|
+
path.join(metadataDir, PROJECT_METADATA_FILE),
|
|
1155
|
+
JSON.stringify(
|
|
1156
|
+
{
|
|
1157
|
+
schemaVersion: 1,
|
|
1158
|
+
buildId: sourceBuildId,
|
|
1159
|
+
readOnly: true,
|
|
1160
|
+
reference: {
|
|
1161
|
+
readOnly: true,
|
|
1162
|
+
forkable: true,
|
|
1163
|
+
sourceBuildId,
|
|
1164
|
+
sourceAppUrl: sourceBuildId ? `${options.siteUrl}/app/${sourceBuildId}` : null,
|
|
1165
|
+
},
|
|
1166
|
+
build: {
|
|
1167
|
+
id: sourceBuildId,
|
|
1168
|
+
title: build?.title || (sourceBuildId ? `Build ${sourceBuildId}` : ""),
|
|
1169
|
+
role: "reference",
|
|
1170
|
+
ownerUsername: build?.ownerUsername || null,
|
|
1171
|
+
collaborationMode: build?.collaborationMode || "open_source",
|
|
1172
|
+
canWrite: false,
|
|
1173
|
+
canPublish: false,
|
|
1174
|
+
},
|
|
1175
|
+
apiUrl: options.apiUrl,
|
|
1176
|
+
siteUrl: options.siteUrl,
|
|
1177
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
1178
|
+
manifest,
|
|
1179
|
+
pulledAt,
|
|
1180
|
+
},
|
|
1181
|
+
null,
|
|
1182
|
+
2,
|
|
1183
|
+
),
|
|
1184
|
+
"utf8",
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
979
1188
|
async function findLocalProjectMetadata(startDir) {
|
|
980
1189
|
let current = path.resolve(startDir || process.cwd());
|
|
981
1190
|
while (true) {
|
|
@@ -1002,6 +1211,29 @@ function resolveProjectDirForSave({ options, localProject }) {
|
|
|
1002
1211
|
return process.cwd();
|
|
1003
1212
|
}
|
|
1004
1213
|
|
|
1214
|
+
function assertLocalProjectCanBeSaved(localProject) {
|
|
1215
|
+
const metadata = localProject?.metadata;
|
|
1216
|
+
if (!metadata) return;
|
|
1217
|
+
if (isReadOnlyReferenceMetadata(metadata)) {
|
|
1218
|
+
const sourceBuildId =
|
|
1219
|
+
Number(metadata.reference?.sourceBuildId || 0) ||
|
|
1220
|
+
Number(metadata.buildId || 0) ||
|
|
1221
|
+
Number(metadata.build?.id || 0) ||
|
|
1222
|
+
0;
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function isReadOnlyReferenceMetadata(metadata) {
|
|
1230
|
+
return (
|
|
1231
|
+
metadata?.readOnly === true ||
|
|
1232
|
+
metadata?.reference?.readOnly === true ||
|
|
1233
|
+
metadata?.build?.role === "reference"
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1005
1237
|
function resolveLocalProjectFilePath({ rootDir, projectPath }) {
|
|
1006
1238
|
const relativePath = projectPathToRelativePath(projectPath);
|
|
1007
1239
|
const root = path.resolve(rootDir);
|
|
@@ -1037,6 +1269,20 @@ async function saveSelectedBuild({ options, auth, build }) {
|
|
|
1037
1269
|
});
|
|
1038
1270
|
}
|
|
1039
1271
|
|
|
1272
|
+
function serializeLumineCliMetadata(options) {
|
|
1273
|
+
const info = options.lumineCli || {};
|
|
1274
|
+
return {
|
|
1275
|
+
packageName: info.packageName || "@stage5/lumine",
|
|
1276
|
+
version: info.version || null,
|
|
1277
|
+
latestVersion: info.latestVersion || null,
|
|
1278
|
+
updateAvailable: Boolean(info.updateAvailable),
|
|
1279
|
+
updateCommand: info.updateCommand || "npx @stage5/lumine@latest",
|
|
1280
|
+
checkedAt: info.checkedAt || null,
|
|
1281
|
+
checkFailed: Boolean(info.checkFailed),
|
|
1282
|
+
checkSkipped: Boolean(info.checkSkipped),
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1040
1286
|
function printBuildList(builds) {
|
|
1041
1287
|
if (!builds.length) {
|
|
1042
1288
|
console.log("No owned or team Twinkle builds found.");
|
|
@@ -1048,6 +1294,23 @@ function printBuildList(builds) {
|
|
|
1048
1294
|
});
|
|
1049
1295
|
}
|
|
1050
1296
|
|
|
1297
|
+
function printOpenSourceBuildList(builds, options) {
|
|
1298
|
+
if (!builds.length) {
|
|
1299
|
+
const searchText = options.searchQuery
|
|
1300
|
+
? ` matching "${options.searchQuery}"`
|
|
1301
|
+
: "";
|
|
1302
|
+
console.log(`No public open-source Twinkle builds found${searchText}.`);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const searchText = options.searchQuery ? ` for "${options.searchQuery}"` : "";
|
|
1306
|
+
console.log(`Public open-source Twinkle builds${searchText}:`);
|
|
1307
|
+
builds.forEach((build, index) => {
|
|
1308
|
+
console.log(`${index + 1}. ${formatOpenSourceBuildListItem(build)}`);
|
|
1309
|
+
});
|
|
1310
|
+
console.log("Reference: lumine reference <build-id>");
|
|
1311
|
+
console.log("Fork: lumine fork <build-id>");
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1051
1314
|
function formatBuildListItem(build) {
|
|
1052
1315
|
const role =
|
|
1053
1316
|
build.role === "owner"
|
|
@@ -1057,6 +1320,16 @@ function formatBuildListItem(build) {
|
|
|
1057
1320
|
return `${formatBuildTitle(build)} - ${role}, ${published}`;
|
|
1058
1321
|
}
|
|
1059
1322
|
|
|
1323
|
+
function formatOpenSourceBuildListItem(build) {
|
|
1324
|
+
const owner = build.ownerUsername ? ` by ${build.ownerUsername}` : "";
|
|
1325
|
+
const stats = [
|
|
1326
|
+
`${Math.max(0, Number(build.forkCount || 0))} forks`,
|
|
1327
|
+
`${Math.max(0, Number(build.viewCount || 0))} views`,
|
|
1328
|
+
].join(", ");
|
|
1329
|
+
const appUrl = build.appUrl ? ` - ${build.appUrl}` : "";
|
|
1330
|
+
return `${formatBuildTitle(build)}${owner} - ${stats}${appUrl}`;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1060
1333
|
function formatBuildTitle(build) {
|
|
1061
1334
|
return `${build.title || `Build ${build.id}`} (#${build.id})`;
|
|
1062
1335
|
}
|
|
@@ -1095,6 +1368,39 @@ function printPullResult(result) {
|
|
|
1095
1368
|
}
|
|
1096
1369
|
}
|
|
1097
1370
|
|
|
1371
|
+
function printReferenceResult(result) {
|
|
1372
|
+
const build = result.build || {};
|
|
1373
|
+
const sourceBuildId =
|
|
1374
|
+
Number(result.reference?.sourceBuildId || 0) || Number(build.id || 0);
|
|
1375
|
+
const entryPath = result.manifest?.entryPath || "unknown";
|
|
1376
|
+
console.log(`Referenced ${formatBuildTitle(build)}.`);
|
|
1377
|
+
console.log(
|
|
1378
|
+
`Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
|
|
1379
|
+
);
|
|
1380
|
+
console.log(`Entry: ${entryPath}`);
|
|
1381
|
+
console.log("Mode: read-only reference");
|
|
1382
|
+
console.log(`Next: cd ${shellQuote(result.dir)}`);
|
|
1383
|
+
if (sourceBuildId) {
|
|
1384
|
+
console.log(`Start from this app: lumine fork ${sourceBuildId}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function printForkResult({ forkResult, pullResult }) {
|
|
1389
|
+
const build = pullResult.build || forkResult.build || {};
|
|
1390
|
+
const sourceBuildId =
|
|
1391
|
+
Number(forkResult.sourceBuild?.id || 0) ||
|
|
1392
|
+
Number(forkResult.sourceBuild?.contentId || 0) ||
|
|
1393
|
+
0;
|
|
1394
|
+
console.log(
|
|
1395
|
+
forkResult.alreadyExists
|
|
1396
|
+
? `Using your existing fork ${formatBuildTitle(build)}.`
|
|
1397
|
+
: `Forked Build ${sourceBuildId || "source"} into ${formatBuildTitle(
|
|
1398
|
+
build,
|
|
1399
|
+
)}.`,
|
|
1400
|
+
);
|
|
1401
|
+
printPullResult(pullResult);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1098
1404
|
function printSaveResult({ result, build, dir, files }) {
|
|
1099
1405
|
const entryPath = result.projectManifest?.entryPath || "unknown";
|
|
1100
1406
|
const version = result.artifactVersion?.versionNumber
|
|
@@ -1222,6 +1528,117 @@ async function request({ method = "GET", url, authToken, body, timeoutMs }) {
|
|
|
1222
1528
|
}
|
|
1223
1529
|
}
|
|
1224
1530
|
|
|
1531
|
+
async function loadLumineCliVersionInfo({ options }) {
|
|
1532
|
+
const packageMetadata = await loadLocalPackageMetadata();
|
|
1533
|
+
const packageName = packageMetadata.name || "@stage5/lumine";
|
|
1534
|
+
const version = packageMetadata.version || null;
|
|
1535
|
+
return {
|
|
1536
|
+
packageName,
|
|
1537
|
+
version,
|
|
1538
|
+
latestVersion: null,
|
|
1539
|
+
updateAvailable: false,
|
|
1540
|
+
updateCommand: `npx ${packageName}@latest`,
|
|
1541
|
+
checkedAt: null,
|
|
1542
|
+
checkFailed: false,
|
|
1543
|
+
checkSkipped: !options.updateCheck,
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
async function loadLocalPackageMetadata() {
|
|
1548
|
+
try {
|
|
1549
|
+
const rawPackage = await fs.readFile(PACKAGE_METADATA_URL, "utf8");
|
|
1550
|
+
const parsedPackage = JSON.parse(rawPackage);
|
|
1551
|
+
return {
|
|
1552
|
+
name: String(parsedPackage?.name || "").trim(),
|
|
1553
|
+
version: String(parsedPackage?.version || "").trim(),
|
|
1554
|
+
};
|
|
1555
|
+
} catch {
|
|
1556
|
+
return {
|
|
1557
|
+
name: "@stage5/lumine",
|
|
1558
|
+
version: null,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function maybeCheckForLumineCliUpdate({ options }) {
|
|
1564
|
+
const info = options.lumineCli || (await loadLumineCliVersionInfo({ options }));
|
|
1565
|
+
const checkedAt = new Date().toISOString();
|
|
1566
|
+
if (!info.packageName || !info.version) {
|
|
1567
|
+
options.lumineCli = {
|
|
1568
|
+
...info,
|
|
1569
|
+
checkedAt,
|
|
1570
|
+
checkFailed: true,
|
|
1571
|
+
checkSkipped: false,
|
|
1572
|
+
};
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
try {
|
|
1577
|
+
const latestVersion = await loadLatestPackageVersion({
|
|
1578
|
+
packageName: info.packageName,
|
|
1579
|
+
registryUrl: options.npmRegistryUrl,
|
|
1580
|
+
});
|
|
1581
|
+
const updateAvailable = isNewerVersion(latestVersion, info.version);
|
|
1582
|
+
options.lumineCli = {
|
|
1583
|
+
...info,
|
|
1584
|
+
latestVersion,
|
|
1585
|
+
updateAvailable,
|
|
1586
|
+
checkedAt,
|
|
1587
|
+
checkFailed: false,
|
|
1588
|
+
checkSkipped: false,
|
|
1589
|
+
};
|
|
1590
|
+
if (updateAvailable) {
|
|
1591
|
+
printLumineCliUpdateWarning(options.lumineCli);
|
|
1592
|
+
}
|
|
1593
|
+
} catch {
|
|
1594
|
+
options.lumineCli = {
|
|
1595
|
+
...info,
|
|
1596
|
+
checkedAt,
|
|
1597
|
+
checkFailed: true,
|
|
1598
|
+
checkSkipped: false,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async function loadLatestPackageVersion({ packageName, registryUrl }) {
|
|
1604
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
1605
|
+
const result = await requestJson({
|
|
1606
|
+
url: `${registryUrl}/${encodedPackageName}/latest`,
|
|
1607
|
+
timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
|
|
1608
|
+
});
|
|
1609
|
+
const latestVersion = String(result?.version || "").trim();
|
|
1610
|
+
if (!latestVersion) {
|
|
1611
|
+
throw new Error("No latest package version returned");
|
|
1612
|
+
}
|
|
1613
|
+
return latestVersion;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function isNewerVersion(latestVersion, currentVersion) {
|
|
1617
|
+
const latestParts = parseSemverParts(latestVersion);
|
|
1618
|
+
const currentParts = parseSemverParts(currentVersion);
|
|
1619
|
+
if (!latestParts || !currentParts) return false;
|
|
1620
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1621
|
+
if (latestParts[index] > currentParts[index]) return true;
|
|
1622
|
+
if (latestParts[index] < currentParts[index]) return false;
|
|
1623
|
+
}
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function parseSemverParts(value) {
|
|
1628
|
+
const match = String(value || "")
|
|
1629
|
+
.trim()
|
|
1630
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
1631
|
+
if (!match) return null;
|
|
1632
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function printLumineCliUpdateWarning(info) {
|
|
1636
|
+
console.error(
|
|
1637
|
+
`lumine: update available for ${info.packageName}: ${info.version} -> ${info.latestVersion}.`,
|
|
1638
|
+
);
|
|
1639
|
+
console.error(`lumine: run \`${info.updateCommand}\` to use the latest CLI.`);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1225
1642
|
function parseArgs(args) {
|
|
1226
1643
|
const firstArg = args[0] || "";
|
|
1227
1644
|
const firstArgIsCommand =
|
|
@@ -1236,7 +1653,13 @@ function parseArgs(args) {
|
|
|
1236
1653
|
const rest = command === "workspace" ? args : args.slice(1);
|
|
1237
1654
|
const raw = {};
|
|
1238
1655
|
const positional = [];
|
|
1239
|
-
const booleanFlags = new Set([
|
|
1656
|
+
const booleanFlags = new Set([
|
|
1657
|
+
"noOpen",
|
|
1658
|
+
"open",
|
|
1659
|
+
"publish",
|
|
1660
|
+
"save",
|
|
1661
|
+
"noUpdateCheck",
|
|
1662
|
+
]);
|
|
1240
1663
|
|
|
1241
1664
|
for (let i = 0; i < rest.length; i += 1) {
|
|
1242
1665
|
const arg = rest[i];
|
|
@@ -1263,12 +1686,26 @@ function parseArgs(args) {
|
|
|
1263
1686
|
return {
|
|
1264
1687
|
command,
|
|
1265
1688
|
target: raw.url || raw.target || positional[0] || "",
|
|
1689
|
+
searchQuery:
|
|
1690
|
+
String(
|
|
1691
|
+
raw.search ||
|
|
1692
|
+
raw.query ||
|
|
1693
|
+
(command === "explore" ? positional.join(" ") : ""),
|
|
1694
|
+
).trim() || "",
|
|
1695
|
+
sort: normalizeOpenSourceSort(raw.sort),
|
|
1266
1696
|
apiUrl: trimTrailingSlash(
|
|
1267
1697
|
String(raw.apiUrl || process.env.TWINKLE_API_URL || DEFAULT_API_URL),
|
|
1268
1698
|
),
|
|
1269
1699
|
siteUrl: trimTrailingSlash(
|
|
1270
1700
|
String(raw.siteUrl || process.env.TWINKLE_SITE_URL || DEFAULT_SITE_URL),
|
|
1271
1701
|
),
|
|
1702
|
+
npmRegistryUrl: trimTrailingSlash(
|
|
1703
|
+
String(
|
|
1704
|
+
raw.npmRegistryUrl ||
|
|
1705
|
+
process.env.LUMINE_NPM_REGISTRY_URL ||
|
|
1706
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
1707
|
+
),
|
|
1708
|
+
),
|
|
1272
1709
|
authFile: String(
|
|
1273
1710
|
raw.authFile || process.env.TWINKLE_CLI_AUTH_FILE || DEFAULT_AUTH_FILE,
|
|
1274
1711
|
),
|
|
@@ -1291,6 +1728,7 @@ function parseArgs(args) {
|
|
|
1291
1728
|
openBrowser: parseBoolean(raw.noOpen, false)
|
|
1292
1729
|
? false
|
|
1293
1730
|
: parseBoolean(raw.open, true),
|
|
1731
|
+
updateCheck: parseBoolean(raw.noUpdateCheck, false) ? false : true,
|
|
1294
1732
|
timeoutMs: Math.max(
|
|
1295
1733
|
Number(raw.timeoutMs || process.env.TWINKLE_TIMEOUT_MS) ||
|
|
1296
1734
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -1312,6 +1750,19 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
1312
1750
|
(await findLocalProjectMetadata(
|
|
1313
1751
|
path.resolve(options.dir || process.cwd()),
|
|
1314
1752
|
));
|
|
1753
|
+
if (
|
|
1754
|
+
resolvedLocalProject?.metadata &&
|
|
1755
|
+
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
1756
|
+
) {
|
|
1757
|
+
const sourceBuildId =
|
|
1758
|
+
Number(resolvedLocalProject.metadata.reference?.sourceBuildId || 0) ||
|
|
1759
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1760
|
+
Number(resolvedLocalProject.metadata.build?.id || 0) ||
|
|
1761
|
+
0;
|
|
1762
|
+
throw new Error(
|
|
1763
|
+
`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.`,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1315
1766
|
const localBuildId = Number(resolvedLocalProject?.metadata?.buildId || 0);
|
|
1316
1767
|
if (localBuildId > 0) return localBuildId;
|
|
1317
1768
|
const selectedBuildId = Number(auth?.selectedBuildId || 0);
|
|
@@ -1403,6 +1854,14 @@ function parseBoolean(value, fallback) {
|
|
|
1403
1854
|
return fallback;
|
|
1404
1855
|
}
|
|
1405
1856
|
|
|
1857
|
+
function normalizeOpenSourceSort(value) {
|
|
1858
|
+
const normalized = String(value || "")
|
|
1859
|
+
.trim()
|
|
1860
|
+
.toLowerCase();
|
|
1861
|
+
if (["recent", "popular", "forks"].includes(normalized)) return normalized;
|
|
1862
|
+
return "forks";
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1406
1865
|
async function openBrowser(url) {
|
|
1407
1866
|
const command =
|
|
1408
1867
|
process.platform === "darwin"
|
|
@@ -1435,6 +1894,12 @@ function defaultWorkspaceDir(build) {
|
|
|
1435
1894
|
return `twinkle-${titleSlug || "build"}-${buildId}`;
|
|
1436
1895
|
}
|
|
1437
1896
|
|
|
1897
|
+
function defaultReferenceDir(build) {
|
|
1898
|
+
const titleSlug = slugify(build?.title || "");
|
|
1899
|
+
const buildId = Number(build?.id || 0) || "build";
|
|
1900
|
+
return `twinkle-reference-${titleSlug || "build"}-${buildId}`;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1438
1903
|
function slugify(value) {
|
|
1439
1904
|
return String(value || "")
|
|
1440
1905
|
.toLowerCase()
|
|
@@ -1454,8 +1919,11 @@ function printHelp() {
|
|
|
1454
1919
|
lumine whoami
|
|
1455
1920
|
lumine logout
|
|
1456
1921
|
lumine projects
|
|
1922
|
+
lumine explore [search terms]
|
|
1457
1923
|
lumine select [twinkle-build-url]
|
|
1458
1924
|
lumine pull [twinkle-build-url]
|
|
1925
|
+
lumine reference <twinkle-build-url>
|
|
1926
|
+
lumine fork <twinkle-build-url>
|
|
1459
1927
|
lumine save
|
|
1460
1928
|
lumine check [twinkle-build-url]
|
|
1461
1929
|
lumine launch [twinkle-build-url]
|
|
@@ -1463,6 +1931,9 @@ function printHelp() {
|
|
|
1463
1931
|
Examples:
|
|
1464
1932
|
npx @stage5/lumine@latest
|
|
1465
1933
|
npx @stage5/lumine@latest login
|
|
1934
|
+
npx @stage5/lumine@latest explore --sort forks
|
|
1935
|
+
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
1936
|
+
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
1466
1937
|
npx @stage5/lumine@latest pull
|
|
1467
1938
|
npx @stage5/lumine@latest save
|
|
1468
1939
|
npx @stage5/lumine@latest save --publish
|
|
@@ -1476,9 +1947,12 @@ Options:
|
|
|
1476
1947
|
--auth-token <token> Override saved login
|
|
1477
1948
|
--dir <path> Directory for pulled project files
|
|
1478
1949
|
--summary <text> Save summary
|
|
1950
|
+
--search <text> Search public open-source Builds
|
|
1951
|
+
--sort <sort> Sort open-source Builds: forks, popular, recent
|
|
1479
1952
|
--publish Publish after saving
|
|
1480
1953
|
--save Save local files before launch
|
|
1481
1954
|
--limit <number> Number of projects to show
|
|
1955
|
+
--no-update-check Skip the npm latest-version check
|
|
1482
1956
|
--no-open Print the approval URL without opening a browser
|
|
1483
1957
|
`);
|
|
1484
1958
|
}
|