@stage5/lumine 0.2.6 → 0.2.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/lib/api.js +23 -0
- package/lib/commands.js +353 -1
- package/lib/constants.js +30 -0
- package/lib/doctor.js +720 -0
- package/lib/util.js +7 -0
- package/lib/workspace.js +56 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +35 -3
package/lib/api.js
CHANGED
|
@@ -68,6 +68,29 @@ export async function loadBuildFiles({ options, auth, buildId, includeContent })
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export async function loadBuildVersions({ options, auth, buildId, limit }) {
|
|
72
|
+
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/versions`);
|
|
73
|
+
if (limit) url.searchParams.set("limit", String(limit));
|
|
74
|
+
return await requestJson({
|
|
75
|
+
url: url.toString(),
|
|
76
|
+
authToken: auth.token,
|
|
77
|
+
timeoutMs: options.timeoutMs,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function loadBuildVersionFiles({
|
|
82
|
+
options,
|
|
83
|
+
auth,
|
|
84
|
+
buildId,
|
|
85
|
+
versionNumber,
|
|
86
|
+
}) {
|
|
87
|
+
return await requestJson({
|
|
88
|
+
url: `${options.apiUrl}/cli/build/${buildId}/versions/${versionNumber}/files`,
|
|
89
|
+
authToken: auth.token,
|
|
90
|
+
timeoutMs: options.timeoutMs,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
export async function resolveBranchBuild({
|
|
72
95
|
options,
|
|
73
96
|
auth,
|
package/lib/commands.js
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
DEFAULT_TIMEOUT_MS,
|
|
15
15
|
LUMINE_MAIN_CHECKOUT_INSTRUCTIONS,
|
|
16
16
|
LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
17
|
+
LUMINE_VERSION_CHECKOUT_INSTRUCTIONS,
|
|
18
|
+
LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
17
19
|
MAIN_CHECKOUT_READONLY_COMMANDS,
|
|
18
20
|
PROJECT_METADATA_DIR,
|
|
19
21
|
SDK_REFERENCE_FILE,
|
|
@@ -26,6 +28,8 @@ import {
|
|
|
26
28
|
listOpenSourceBuilds,
|
|
27
29
|
loadBuildFiles,
|
|
28
30
|
loadBuildMetadata,
|
|
31
|
+
loadBuildVersionFiles,
|
|
32
|
+
loadBuildVersions,
|
|
29
33
|
loadContributionDiff,
|
|
30
34
|
loadLumineCliVersionInfo,
|
|
31
35
|
loadOpenSourceBuildFiles,
|
|
@@ -50,13 +54,16 @@ import {
|
|
|
50
54
|
} from "./auth.js";
|
|
51
55
|
import { probeUrl, requestJson } from "./http.js";
|
|
52
56
|
import { sdkCommand } from "./sdk.js";
|
|
57
|
+
import { doctorCommand, normalizePreviewUrl } from "./doctor.js";
|
|
53
58
|
import {
|
|
54
59
|
defaultMainCheckoutDir,
|
|
55
60
|
defaultReferenceDir,
|
|
61
|
+
defaultVersionCheckoutDir,
|
|
56
62
|
defaultWorkspaceDir,
|
|
57
63
|
parseBoolean,
|
|
58
64
|
resolveBuildId,
|
|
59
65
|
resolveBuildReference,
|
|
66
|
+
resolveRequiredBuildId,
|
|
60
67
|
shellQuote,
|
|
61
68
|
toCamelCase,
|
|
62
69
|
trimTrailingSlash,
|
|
@@ -75,6 +82,7 @@ import {
|
|
|
75
82
|
writeInstructionFiles,
|
|
76
83
|
writeMainCheckoutMetadata,
|
|
77
84
|
writeProjectFiles,
|
|
85
|
+
writeVersionCheckoutMetadata,
|
|
78
86
|
writeProjectMetadata,
|
|
79
87
|
writeReferenceInstructions,
|
|
80
88
|
writeReferenceMetadata,
|
|
@@ -160,6 +168,14 @@ export async function main() {
|
|
|
160
168
|
await check(options);
|
|
161
169
|
return;
|
|
162
170
|
}
|
|
171
|
+
if (options.command === "versions") {
|
|
172
|
+
await versionsCommand(options);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (options.command === "restore") {
|
|
176
|
+
await restoreVersion(options);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
163
179
|
if (options.command === "launch") {
|
|
164
180
|
await launch(options);
|
|
165
181
|
return;
|
|
@@ -176,6 +192,10 @@ export async function main() {
|
|
|
176
192
|
await thumbnailCommand(options);
|
|
177
193
|
return;
|
|
178
194
|
}
|
|
195
|
+
if (options.command === "doctor") {
|
|
196
|
+
await doctorCommand(options);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
179
199
|
|
|
180
200
|
printHelp();
|
|
181
201
|
}
|
|
@@ -250,6 +270,11 @@ export async function selectProject(options) {
|
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
export async function pull(options) {
|
|
273
|
+
if (options.versionProvided && !(options.pullVersion > 0)) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"Pass a save number: `lumine pull --version <n>` (run `lumine versions` to see save numbers).",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
253
278
|
const auth = await resolveAuth(options);
|
|
254
279
|
const localProject = await findLocalProjectMetadata(
|
|
255
280
|
path.resolve(options.dir || process.cwd()),
|
|
@@ -264,6 +289,10 @@ export async function pull(options) {
|
|
|
264
289
|
auth,
|
|
265
290
|
buildId: requestedBuildId,
|
|
266
291
|
});
|
|
292
|
+
if (options.pullVersion > 0) {
|
|
293
|
+
await pullVersionBuildFiles({ options, auth, build: selectedBuild });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
267
296
|
if (options.pullMain) {
|
|
268
297
|
await pullMainBuildFiles({ options, auth, build: selectedBuild });
|
|
269
298
|
return;
|
|
@@ -681,6 +710,18 @@ export function shouldUseContributionBranch(build) {
|
|
|
681
710
|
);
|
|
682
711
|
}
|
|
683
712
|
|
|
713
|
+
// A canonical team build the viewer contributes to: plain pull/restore would
|
|
714
|
+
// switch to their contribution branch, so version-history follow-up commands
|
|
715
|
+
// for THIS build's history need --main. An owner's canonical build is their
|
|
716
|
+
// editable workspace and never needs it.
|
|
717
|
+
export function isCollaboratorMainBuild(build) {
|
|
718
|
+
return (
|
|
719
|
+
build?.role === "collaborator" &&
|
|
720
|
+
build.canWrite !== true &&
|
|
721
|
+
!Number(build?.contributionRootBuildId || 0)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
684
725
|
export async function ensureDefaultContributionBranch({ options, auth, build }) {
|
|
685
726
|
const rootBuildId =
|
|
686
727
|
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
@@ -919,6 +960,269 @@ export async function pullMainBuildFiles({ options, auth, build }) {
|
|
|
919
960
|
);
|
|
920
961
|
}
|
|
921
962
|
|
|
963
|
+
// Read-only checkout of ONE previous save (`lumine pull --version <n>`).
|
|
964
|
+
// Without --main it targets the build the workspace edits (your branch on team
|
|
965
|
+
// projects); with --main it targets the team project's main history.
|
|
966
|
+
export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
967
|
+
const versionNumber = options.pullVersion;
|
|
968
|
+
let targetBuild = build;
|
|
969
|
+
if (options.pullMain) {
|
|
970
|
+
const rootBuildId =
|
|
971
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
972
|
+
targetBuild =
|
|
973
|
+
rootBuildId === Number(build?.id || 0)
|
|
974
|
+
? build
|
|
975
|
+
: await loadBuildMetadata({ options, auth, buildId: rootBuildId });
|
|
976
|
+
} else {
|
|
977
|
+
targetBuild = await resolveEditableWorkspaceBuild({
|
|
978
|
+
options,
|
|
979
|
+
auth,
|
|
980
|
+
build,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
const buildId = Number(targetBuild?.id || 0);
|
|
984
|
+
const result = await loadBuildVersionFiles({
|
|
985
|
+
options,
|
|
986
|
+
auth,
|
|
987
|
+
buildId,
|
|
988
|
+
versionNumber,
|
|
989
|
+
});
|
|
990
|
+
const resolvedBuild = result.build || targetBuild;
|
|
991
|
+
const version = result.version || { version: versionNumber };
|
|
992
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
993
|
+
// Same nesting rules as a main checkout: never create a version checkout
|
|
994
|
+
// inside another Lumine workspace; re-pulling the same checkout refreshes it
|
|
995
|
+
// in place.
|
|
996
|
+
const enclosing = options.dir
|
|
997
|
+
? null
|
|
998
|
+
: await findLocalProjectMetadata(process.cwd());
|
|
999
|
+
const enclosingIsThisCheckout =
|
|
1000
|
+
enclosing?.metadata?.versionCheckout === true &&
|
|
1001
|
+
Number(enclosing.metadata.buildId || 0) === buildId &&
|
|
1002
|
+
Number(enclosing.metadata.checkoutVersion || 0) ===
|
|
1003
|
+
Number(version.version || 0);
|
|
1004
|
+
const dirName = defaultVersionCheckoutDir(resolvedBuild, version.version);
|
|
1005
|
+
const dir = path.resolve(
|
|
1006
|
+
options.dir ||
|
|
1007
|
+
(enclosingIsThisCheckout
|
|
1008
|
+
? enclosing.rootDir
|
|
1009
|
+
: enclosing
|
|
1010
|
+
? path.join(path.dirname(enclosing.rootDir), dirName)
|
|
1011
|
+
: dirName),
|
|
1012
|
+
);
|
|
1013
|
+
await writeProjectFiles({ dir, files });
|
|
1014
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1015
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
1016
|
+
}
|
|
1017
|
+
await writeInstructionFiles({
|
|
1018
|
+
dir,
|
|
1019
|
+
marker: LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
1020
|
+
content: LUMINE_VERSION_CHECKOUT_INSTRUCTIONS,
|
|
1021
|
+
});
|
|
1022
|
+
await writeSdkReference({ dir });
|
|
1023
|
+
await writeVersionCheckoutMetadata({
|
|
1024
|
+
dir,
|
|
1025
|
+
options,
|
|
1026
|
+
build: resolvedBuild,
|
|
1027
|
+
manifest: result.projectManifest || null,
|
|
1028
|
+
version,
|
|
1029
|
+
pulledAt: new Date().toISOString(),
|
|
1030
|
+
});
|
|
1031
|
+
const summaryText = version.summary ? ` — "${version.summary}"` : "";
|
|
1032
|
+
console.log(
|
|
1033
|
+
`Pulled save v${version.version} of ${formatBuildTitle(resolvedBuild)} (read-only)${summaryText}`,
|
|
1034
|
+
);
|
|
1035
|
+
console.log(
|
|
1036
|
+
`Pulled ${files.length} file${files.length === 1 ? "" : "s"} to ${dir}`,
|
|
1037
|
+
);
|
|
1038
|
+
console.log(
|
|
1039
|
+
isCollaboratorMainBuild(resolvedBuild)
|
|
1040
|
+
? `Restore it with \`lumine restore ${version.version} --main\` from your branch workspace, then \`lumine save\`.`
|
|
1041
|
+
: `Restore it with \`lumine restore ${version.version}\` from Build ${buildId}'s editable workspace, then \`lumine save\`.`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// List a build's previous saves (every workspace/CLI save creates one).
|
|
1046
|
+
// Targets the SAME build `lumine pull --version <n>` would snapshot, so the
|
|
1047
|
+
// listed numbers and the printed follow-up commands always agree: without
|
|
1048
|
+
// --main that's the editable workspace build (a collaborator's branch on team
|
|
1049
|
+
// projects), with --main the team project's main.
|
|
1050
|
+
export async function versionsCommand(options) {
|
|
1051
|
+
const auth = await resolveAuth(options);
|
|
1052
|
+
const localProject = await findLocalProjectMetadata(
|
|
1053
|
+
path.resolve(options.dir || process.cwd()),
|
|
1054
|
+
);
|
|
1055
|
+
const requestedBuildId = options.buildIdFlag
|
|
1056
|
+
? resolveRequiredBuildId(options.buildIdFlag)
|
|
1057
|
+
: await resolveRequiredBuildIdOrSelected(options, auth, { localProject });
|
|
1058
|
+
const requestedBuild = await loadBuildMetadata({
|
|
1059
|
+
options,
|
|
1060
|
+
auth,
|
|
1061
|
+
buildId: requestedBuildId,
|
|
1062
|
+
});
|
|
1063
|
+
// Standing in a read-only checkout (pull --main / pull --version), the
|
|
1064
|
+
// resolved id IS the build being looked at — don't bounce it to the
|
|
1065
|
+
// viewer's contribution branch.
|
|
1066
|
+
const resolvedFromReadOnlyCheckout =
|
|
1067
|
+
!options.buildIdFlag &&
|
|
1068
|
+
!(resolveBuildReference(options.target).buildId > 0) &&
|
|
1069
|
+
(localProject?.metadata?.mainCheckout === true ||
|
|
1070
|
+
localProject?.metadata?.versionCheckout === true);
|
|
1071
|
+
let buildId = requestedBuildId;
|
|
1072
|
+
if (options.pullMain) {
|
|
1073
|
+
buildId =
|
|
1074
|
+
Number(requestedBuild?.contributionRootBuildId || 0) || requestedBuildId;
|
|
1075
|
+
} else if (!resolvedFromReadOnlyCheckout) {
|
|
1076
|
+
const editableBuild = await resolveEditableWorkspaceBuild({
|
|
1077
|
+
options,
|
|
1078
|
+
auth,
|
|
1079
|
+
build: requestedBuild,
|
|
1080
|
+
});
|
|
1081
|
+
buildId = Number(editableBuild?.id || 0) || requestedBuildId;
|
|
1082
|
+
}
|
|
1083
|
+
const result = await loadBuildVersions({
|
|
1084
|
+
options,
|
|
1085
|
+
auth,
|
|
1086
|
+
buildId,
|
|
1087
|
+
limit: options.limit,
|
|
1088
|
+
});
|
|
1089
|
+
printVersionList({ result });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export function printVersionList({ result }) {
|
|
1093
|
+
const build = result.build || {};
|
|
1094
|
+
const versions = Array.isArray(result.versions) ? result.versions : [];
|
|
1095
|
+
if (!versions.length) {
|
|
1096
|
+
console.log(`No previous saves found for ${formatBuildTitle(build)}.`);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
console.log(
|
|
1100
|
+
`Previous saves for ${formatBuildTitle(build)} (newest first):`,
|
|
1101
|
+
);
|
|
1102
|
+
for (const version of versions) {
|
|
1103
|
+
const author =
|
|
1104
|
+
version.createdByUsername ||
|
|
1105
|
+
(version.createdByRole === "assistant" ? "Lumine" : null) ||
|
|
1106
|
+
version.createdByRole ||
|
|
1107
|
+
"unknown";
|
|
1108
|
+
const fileText =
|
|
1109
|
+
Number(version.fileCount || 0) > 0
|
|
1110
|
+
? `${version.fileCount} file${Number(version.fileCount) === 1 ? "" : "s"}`
|
|
1111
|
+
: "single-file save";
|
|
1112
|
+
const summaryText = version.summary ? ` — ${version.summary}` : "";
|
|
1113
|
+
console.log(
|
|
1114
|
+
` v${version.version} ${formatVersionTimestamp(version.createdAt)} by ${author} (${fileText})${summaryText}`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
// The follow-up commands must hit exactly the history that was just listed,
|
|
1118
|
+
// regardless of where the user runs them or which flags produced this list.
|
|
1119
|
+
// Embedding the listed build id makes pull workspace-independent; --main is
|
|
1120
|
+
// needed only when the listed build is a canonical team build the viewer
|
|
1121
|
+
// contributes to (plain pull/restore would switch to their branch — mirrors
|
|
1122
|
+
// shouldUseContributionBranch). An owner's canonical build never needs
|
|
1123
|
+
// --main: it IS their editable workspace.
|
|
1124
|
+
const listedBuildId = Number(build.id || 0);
|
|
1125
|
+
const collaboratorMain = isCollaboratorMainBuild(build);
|
|
1126
|
+
const pullTarget = listedBuildId ? ` ${listedBuildId}` : "";
|
|
1127
|
+
console.log(
|
|
1128
|
+
`View one read-only: lumine pull${pullTarget} --version <n>${collaboratorMain ? " --main" : ""}`,
|
|
1129
|
+
);
|
|
1130
|
+
console.log(
|
|
1131
|
+
collaboratorMain
|
|
1132
|
+
? `Bring one back: lumine restore <n> --main (run from your branch workspace; then lumine save)`
|
|
1133
|
+
: `Bring one back: lumine restore <n> (run from ${listedBuildId ? `Build ${listedBuildId}'s` : "the build's"} editable workspace; then lumine save)`,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function formatVersionTimestamp(seconds) {
|
|
1138
|
+
const timestamp = Number(seconds || 0);
|
|
1139
|
+
if (!timestamp) return "unknown time";
|
|
1140
|
+
const date = new Date(timestamp * 1000);
|
|
1141
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1142
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Write a previous save's files into the CURRENT editable workspace. Local
|
|
1146
|
+
// only on purpose: the user reviews the result and `lumine save` makes it a
|
|
1147
|
+
// new version (nothing on the server is rewritten in place). With --main in a
|
|
1148
|
+
// branch workspace, the save is read from the team project's MAIN history —
|
|
1149
|
+
// the recovery path when work was lost on main itself.
|
|
1150
|
+
export async function restoreVersion(options) {
|
|
1151
|
+
// Same strictness as `pull --version`: only a plain positive integer may
|
|
1152
|
+
// reach the workspace-overwriting path below. "2.5" must error, not
|
|
1153
|
+
// silently restore v2.
|
|
1154
|
+
const rawInput = String(options.positional[0] ?? "").trim();
|
|
1155
|
+
let versionNumber = 0;
|
|
1156
|
+
if (rawInput) {
|
|
1157
|
+
if (!/^\d+$/.test(rawInput)) {
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
"Pass a plain save number, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
versionNumber = parseInt(rawInput, 10);
|
|
1163
|
+
} else if (options.pullVersion > 0) {
|
|
1164
|
+
// --version <n>, already strict-parsed by parseArgs.
|
|
1165
|
+
versionNumber = options.pullVersion;
|
|
1166
|
+
} else if (options.versionProvided) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Pass a plain save number, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (!(versionNumber > 0)) {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
"Pass the save number to restore, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const auth = await resolveAuth(options);
|
|
1177
|
+
const localProject = await findLocalProjectMetadata(
|
|
1178
|
+
path.resolve(options.dir || process.cwd()),
|
|
1179
|
+
);
|
|
1180
|
+
if (!Number(localProject?.metadata?.buildId || 0)) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
"Run `lumine restore` inside a pulled Lumine workspace (or pass --dir <workspace>).",
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
assertLocalProjectCanBeSaved(localProject);
|
|
1186
|
+
const workspaceBuildId = Number(localProject.metadata.buildId);
|
|
1187
|
+
let sourceBuildId = workspaceBuildId;
|
|
1188
|
+
if (options.pullMain) {
|
|
1189
|
+
const rootBuildId = Number(
|
|
1190
|
+
localProject.metadata.build?.contributionRootBuildId || 0,
|
|
1191
|
+
);
|
|
1192
|
+
if (!rootBuildId) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
"--main restores from the team project's main history, which needs a contribution-branch workspace.",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
sourceBuildId = rootBuildId;
|
|
1198
|
+
}
|
|
1199
|
+
const result = await loadBuildVersionFiles({
|
|
1200
|
+
options,
|
|
1201
|
+
auth,
|
|
1202
|
+
buildId: sourceBuildId,
|
|
1203
|
+
versionNumber,
|
|
1204
|
+
});
|
|
1205
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
1206
|
+
if (!files.length) {
|
|
1207
|
+
throw new Error(`Save v${versionNumber} has no files to restore.`);
|
|
1208
|
+
}
|
|
1209
|
+
const dir = resolveProjectDirForSave({ options, localProject });
|
|
1210
|
+
await writeProjectFiles({ dir, files });
|
|
1211
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1212
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
1213
|
+
}
|
|
1214
|
+
const version = result.version || { version: versionNumber };
|
|
1215
|
+
const sourceBuild = result.build || { id: sourceBuildId };
|
|
1216
|
+
const summaryText = version.summary ? ` ("${version.summary}")` : "";
|
|
1217
|
+
console.log(
|
|
1218
|
+
`Restored ${files.length} file${files.length === 1 ? "" : "s"} from ${formatBuildTitle(sourceBuild)} save v${version.version}${summaryText} into ${dir}`,
|
|
1219
|
+
);
|
|
1220
|
+
console.log("Local only so far — review the files, then run:");
|
|
1221
|
+
console.log(
|
|
1222
|
+
` lumine save --summary ${shellQuote(`Restore from ${options.pullMain ? "main " : ""}v${version.version}`)}`,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
922
1226
|
export function printBuildList(builds) {
|
|
923
1227
|
if (!builds.length) {
|
|
924
1228
|
console.log("No owned or team Twinkle builds found.");
|
|
@@ -1202,6 +1506,9 @@ export function parseArgs(args) {
|
|
|
1202
1506
|
"allowWrite",
|
|
1203
1507
|
"main",
|
|
1204
1508
|
"yes",
|
|
1509
|
+
"json",
|
|
1510
|
+
"keepAssets",
|
|
1511
|
+
"noBrowser",
|
|
1205
1512
|
]);
|
|
1206
1513
|
|
|
1207
1514
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -1270,6 +1577,11 @@ export function parseArgs(args) {
|
|
|
1270
1577
|
siteUrl: trimTrailingSlash(
|
|
1271
1578
|
String(raw.siteUrl || process.env.TWINKLE_SITE_URL || DEFAULT_SITE_URL),
|
|
1272
1579
|
),
|
|
1580
|
+
previewUrl: normalizePreviewUrl(
|
|
1581
|
+
raw.previewUrl ||
|
|
1582
|
+
process.env.TWINKLE_PREVIEW_URL ||
|
|
1583
|
+
process.env.TWINKLE_BUILD_PREVIEW_URL,
|
|
1584
|
+
),
|
|
1273
1585
|
npmRegistryUrl: trimTrailingSlash(
|
|
1274
1586
|
String(
|
|
1275
1587
|
raw.npmRegistryUrl ||
|
|
@@ -1286,6 +1598,14 @@ export function parseArgs(args) {
|
|
|
1286
1598
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
1287
1599
|
dir: raw.dir ? String(raw.dir) : "",
|
|
1288
1600
|
pullMain: parseBoolean(raw.main, false),
|
|
1601
|
+
// Strict: only a plain positive integer counts. Anything else (missing
|
|
1602
|
+
// value, "foo", a swallowed flag like "--main", "2.5") stays 0 and the
|
|
1603
|
+
// consuming command must reject it via versionProvided instead of
|
|
1604
|
+
// silently falling back to a live pull.
|
|
1605
|
+
versionProvided: Object.prototype.hasOwnProperty.call(raw, "version"),
|
|
1606
|
+
pullVersion: /^\d+$/.test(String(raw.version ?? "").trim())
|
|
1607
|
+
? parseInt(String(raw.version).trim(), 10)
|
|
1608
|
+
: 0,
|
|
1289
1609
|
summary: raw.summary ? String(raw.summary) : "",
|
|
1290
1610
|
publish: parseBoolean(raw.publish, false),
|
|
1291
1611
|
saveFirst: parseBoolean(raw.save, false),
|
|
@@ -1300,6 +1620,9 @@ export function parseArgs(args) {
|
|
|
1300
1620
|
openBrowser: parseBoolean(raw.noOpen, false)
|
|
1301
1621
|
? false
|
|
1302
1622
|
: parseBoolean(raw.open, true),
|
|
1623
|
+
json: parseBoolean(raw.json, false),
|
|
1624
|
+
keepAssets: parseBoolean(raw.keepAssets, false),
|
|
1625
|
+
noBrowser: parseBoolean(raw.noBrowser, false),
|
|
1303
1626
|
updateCheck: parseBoolean(raw.noUpdateCheck, false) ? false : true,
|
|
1304
1627
|
timeoutMs: Math.max(
|
|
1305
1628
|
Number(raw.timeoutMs || process.env.TWINKLE_TIMEOUT_MS) ||
|
|
@@ -1344,6 +1667,22 @@ export async function resolveRequiredBuildIdOrSelected(
|
|
|
1344
1667
|
}
|
|
1345
1668
|
if (mainBuildId > 0) return mainBuildId;
|
|
1346
1669
|
}
|
|
1670
|
+
// Same deal for a `pull --version <n>` checkout: read-only commands resolve
|
|
1671
|
+
// the build it snapshots; mutating commands are pointed back at the
|
|
1672
|
+
// editable workspace.
|
|
1673
|
+
if (resolvedLocalProject?.metadata?.versionCheckout === true) {
|
|
1674
|
+
const checkoutBuildId =
|
|
1675
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1676
|
+
Number(resolvedLocalProject.metadata.build?.id || 0);
|
|
1677
|
+
const checkoutVersion =
|
|
1678
|
+
Number(resolvedLocalProject.metadata.checkoutVersion || 0) || 0;
|
|
1679
|
+
if (!MAIN_CHECKOUT_READONLY_COMMANDS.has(options.command)) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`This is a read-only checkout of a previous save${checkoutVersion ? ` (v${checkoutVersion})` : ""}${checkoutBuildId ? ` for Build ${checkoutBuildId}` : ""}; \`lumine ${options.command}\` isn't available here. Run it from the editable workspace, or pass an explicit Build URL.`,
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
if (checkoutBuildId > 0) return checkoutBuildId;
|
|
1685
|
+
}
|
|
1347
1686
|
if (
|
|
1348
1687
|
resolvedLocalProject?.metadata &&
|
|
1349
1688
|
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
@@ -1433,6 +1772,9 @@ export function printHelp() {
|
|
|
1433
1772
|
lumine select [twinkle-build-url]
|
|
1434
1773
|
lumine pull [twinkle-build-url]
|
|
1435
1774
|
lumine pull [twinkle-build-url] --main
|
|
1775
|
+
lumine pull [twinkle-build-url] --version <n> [--main]
|
|
1776
|
+
lumine versions [twinkle-build-url] [--main] [--limit <n>]
|
|
1777
|
+
lumine restore <n> [--main]
|
|
1436
1778
|
lumine reference <twinkle-build-url>
|
|
1437
1779
|
lumine fork <twinkle-build-url>
|
|
1438
1780
|
lumine diff <twinkle-branch-url>
|
|
@@ -1452,6 +1794,7 @@ export function printHelp() {
|
|
|
1452
1794
|
lumine thumbnail set <file>
|
|
1453
1795
|
lumine thumbnail capture [--out <file>]
|
|
1454
1796
|
lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
|
|
1797
|
+
lumine doctor runtime-assets
|
|
1455
1798
|
|
|
1456
1799
|
Examples:
|
|
1457
1800
|
npx @stage5/lumine@latest
|
|
@@ -1464,6 +1807,9 @@ Examples:
|
|
|
1464
1807
|
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
1465
1808
|
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
1466
1809
|
npx @stage5/lumine@latest pull 884 --main
|
|
1810
|
+
npx @stage5/lumine@latest versions --main
|
|
1811
|
+
npx @stage5/lumine@latest pull --version 41 --main
|
|
1812
|
+
npx @stage5/lumine@latest restore 41 --main
|
|
1467
1813
|
npx @stage5/lumine@latest update-from-main
|
|
1468
1814
|
npx @stage5/lumine@latest pull
|
|
1469
1815
|
npx @stage5/lumine@latest save
|
|
@@ -1479,14 +1825,17 @@ Examples:
|
|
|
1479
1825
|
npx @stage5/lumine@latest thumbnail set art/cover.png
|
|
1480
1826
|
npx @stage5/lumine@latest thumbnail capture --out capture.png
|
|
1481
1827
|
npx @stage5/lumine@latest thumbnail generate --model gpt-image-2 --yes
|
|
1828
|
+
npx @stage5/lumine@latest doctor runtime-assets --build 917 --json
|
|
1482
1829
|
|
|
1483
1830
|
Options:
|
|
1484
1831
|
--api-url <url> Twinkle API origin
|
|
1485
1832
|
--site-url <url> Twinkle website origin
|
|
1833
|
+
--preview-url <url> Twinkle Build preview origin
|
|
1486
1834
|
--auth-file <path> Saved login path
|
|
1487
1835
|
--auth-token <token> Override saved login
|
|
1488
1836
|
--dir <path> Directory for pulled project files
|
|
1489
|
-
--main With pull:
|
|
1837
|
+
--main With pull/versions/restore: target the team project's main
|
|
1838
|
+
--version <n> With pull: read-only checkout of previous save v<n>
|
|
1490
1839
|
--title <text> New Build title
|
|
1491
1840
|
--description <text> Optional New Build description
|
|
1492
1841
|
--no-description Skip the New Build description prompt
|
|
@@ -1503,6 +1852,9 @@ Options:
|
|
|
1503
1852
|
--allow-write Permit sdk methods that mutate app data
|
|
1504
1853
|
--path <api/...> Call an sdk endpoint not in the curated list
|
|
1505
1854
|
--scopes <a,b> Override requested build API token scopes
|
|
1855
|
+
--json Print machine-readable output for doctor commands
|
|
1856
|
+
--keep-assets Keep doctor probe assets instead of deleting them
|
|
1857
|
+
--no-browser Skip doctor browser probes
|
|
1506
1858
|
--model <model> Image model for generate: gpt-image-2 or nano-banana (required, no default)
|
|
1507
1859
|
--quality <q> gpt-image-2 quality: low, medium, high (default high)
|
|
1508
1860
|
--name <fileName> File name hint for a generated asset
|
package/lib/constants.js
CHANGED
|
@@ -88,6 +88,8 @@ export const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
|
88
88
|
"<!-- Lumine CLI Reference Instructions -->";
|
|
89
89
|
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
90
90
|
"<!-- Lumine CLI Main Checkout Instructions -->";
|
|
91
|
+
export const LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
92
|
+
"<!-- Lumine CLI Version Checkout Instructions -->";
|
|
91
93
|
export const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
92
94
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
93
95
|
import.meta.url,
|
|
@@ -139,6 +141,12 @@ lumine save --summary "Describe the change"
|
|
|
139
141
|
\`\`\`
|
|
140
142
|
|
|
141
143
|
- Run lumine check before launch when possible.
|
|
144
|
+
- \`lumine versions\` lists this build's previous saves (add --main inside a
|
|
145
|
+
branch workspace for the team project's main history). \`lumine pull
|
|
146
|
+
--version <n>\` checks a previous save out read-only alongside the workspace
|
|
147
|
+
for inspection; \`lumine restore <n>\` writes that save's files into THIS
|
|
148
|
+
workspace (local only — review, then lumine save). Use these to recover work
|
|
149
|
+
lost to a bad edit, merge, or overwrite instead of rebuilding it from memory.
|
|
142
150
|
- Use \`lumine sdk call <namespace.method> '{...}'\` to inspect real endpoint
|
|
143
151
|
data and measure latency (add --repeat <n>); \`lumine sdk list\` shows
|
|
144
152
|
callable methods. It prints the raw HTTP endpoint response, which can differ
|
|
@@ -206,6 +214,7 @@ lumine save --summary "Describe the change"
|
|
|
206
214
|
|
|
207
215
|
- Use local project files with relative or root-local imports only. Do not add package imports, CDN scripts, external network calls, or app-local /api/* routes.
|
|
208
216
|
- Build apps run in sandboxed iframes without allow-forms. Do not use <form> elements, native form submission, requestSubmit(), or browser form navigation. Build input flows with JavaScript-handled inputs and buttons instead.
|
|
217
|
+
- Interface text must not be selectable on touch devices: long-pressing UI on mobile must not highlight it. Apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls). Keep text inputs and genuinely user-copyable content (story text, chat messages, user-written text) selectable. lumine check flags projects whose reachable files have clickable UI but no user-select: none rule.
|
|
209
218
|
- CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (even a decorative background canvas) or game-y words in visible text switch the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. Document-style apps that use a canvas must call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot — any of those opts out of auto game mode — then pad by layout.safeInsets and scroll within layout.viewport.height.
|
|
210
219
|
- For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
|
|
211
220
|
- For Three.js, use import * as THREE from '/build/vendor/three/0.184.0/three.module.min.js';. Addons (OrbitControls, GLTFLoader, ...) live under /build/vendor/three/0.184.0/addons/, e.g. import { OrbitControls } from '/build/vendor/three/0.184.0/addons/controls/OrbitControls.js';. Builds saved with the older /build/vendor/three/0.160.0/ path keep working.
|
|
@@ -240,6 +249,22 @@ the workspace to save.
|
|
|
240
249
|
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
241
250
|
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
242
251
|
`;
|
|
252
|
+
export const LUMINE_VERSION_CHECKOUT_INSTRUCTIONS = `${LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER}
|
|
253
|
+
# Lumine Version Checkout (Read-Only)
|
|
254
|
+
|
|
255
|
+
This directory is a read-only snapshot of ONE PREVIOUS SAVE of a Twinkle Build,
|
|
256
|
+
pulled with \`lumine pull --version <n>\`. Use it to inspect or diff what the
|
|
257
|
+
project looked like at that save; it is not the workspace to edit or save.
|
|
258
|
+
|
|
259
|
+
## Source Of Truth
|
|
260
|
+
|
|
261
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
262
|
+
- metadata.versionCheckout is true here: do not run lumine save from this directory.
|
|
263
|
+
- To bring this save's files back into the live project, run \`lumine restore
|
|
264
|
+
<n>\` from the editable workspace (then \`lumine save\`), or copy the specific
|
|
265
|
+
parts you need into that workspace.
|
|
266
|
+
- Do not edit another local checkout to bypass read-only semantics.
|
|
267
|
+
`;
|
|
243
268
|
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS = `${LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER}
|
|
244
269
|
# Lumine Main Checkout (Read-Only)
|
|
245
270
|
|
|
@@ -268,6 +293,8 @@ export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
|
|
|
268
293
|
// assets never touch main's project files (uploads go to the current
|
|
269
294
|
// user's own asset space), so listing/uploading from a main checkout is safe.
|
|
270
295
|
"assets",
|
|
296
|
+
// versions only reads save history.
|
|
297
|
+
"versions",
|
|
271
298
|
]);
|
|
272
299
|
export const COMMANDS = new Set([
|
|
273
300
|
"workspace",
|
|
@@ -289,8 +316,11 @@ export const COMMANDS = new Set([
|
|
|
289
316
|
"push",
|
|
290
317
|
"check",
|
|
291
318
|
"launch",
|
|
319
|
+
"versions",
|
|
320
|
+
"restore",
|
|
292
321
|
"sdk",
|
|
293
322
|
"assets",
|
|
294
323
|
"thumbnail",
|
|
324
|
+
"doctor",
|
|
295
325
|
"help",
|
|
296
326
|
]);
|