@stage5/lumine 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/lib/api.js +88 -1
- package/lib/assets.js +164 -1
- package/lib/commands.js +354 -3
- package/lib/constants.js +69 -0
- package/lib/thumbnail.js +330 -0
- package/lib/util.js +7 -0
- package/lib/workspace.js +56 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +19 -2
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,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
saveProjectFiles,
|
|
39
43
|
} from "./api.js";
|
|
40
44
|
import { assetsCommand, writeAssetsManifest } from "./assets.js";
|
|
45
|
+
import { thumbnailCommand } from "./thumbnail.js";
|
|
41
46
|
import {
|
|
42
47
|
assertAuthScope,
|
|
43
48
|
ensureAuth,
|
|
@@ -52,10 +57,12 @@ import { sdkCommand } from "./sdk.js";
|
|
|
52
57
|
import {
|
|
53
58
|
defaultMainCheckoutDir,
|
|
54
59
|
defaultReferenceDir,
|
|
60
|
+
defaultVersionCheckoutDir,
|
|
55
61
|
defaultWorkspaceDir,
|
|
56
62
|
parseBoolean,
|
|
57
63
|
resolveBuildId,
|
|
58
64
|
resolveBuildReference,
|
|
65
|
+
resolveRequiredBuildId,
|
|
59
66
|
shellQuote,
|
|
60
67
|
toCamelCase,
|
|
61
68
|
trimTrailingSlash,
|
|
@@ -74,6 +81,7 @@ import {
|
|
|
74
81
|
writeInstructionFiles,
|
|
75
82
|
writeMainCheckoutMetadata,
|
|
76
83
|
writeProjectFiles,
|
|
84
|
+
writeVersionCheckoutMetadata,
|
|
77
85
|
writeProjectMetadata,
|
|
78
86
|
writeReferenceInstructions,
|
|
79
87
|
writeReferenceMetadata,
|
|
@@ -159,6 +167,14 @@ export async function main() {
|
|
|
159
167
|
await check(options);
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
170
|
+
if (options.command === "versions") {
|
|
171
|
+
await versionsCommand(options);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (options.command === "restore") {
|
|
175
|
+
await restoreVersion(options);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
162
178
|
if (options.command === "launch") {
|
|
163
179
|
await launch(options);
|
|
164
180
|
return;
|
|
@@ -171,6 +187,10 @@ export async function main() {
|
|
|
171
187
|
await assetsCommand(options);
|
|
172
188
|
return;
|
|
173
189
|
}
|
|
190
|
+
if (options.command === "thumbnail") {
|
|
191
|
+
await thumbnailCommand(options);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
174
194
|
|
|
175
195
|
printHelp();
|
|
176
196
|
}
|
|
@@ -245,6 +265,11 @@ export async function selectProject(options) {
|
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
export async function pull(options) {
|
|
268
|
+
if (options.versionProvided && !(options.pullVersion > 0)) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
"Pass a save number: `lumine pull --version <n>` (run `lumine versions` to see save numbers).",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
248
273
|
const auth = await resolveAuth(options);
|
|
249
274
|
const localProject = await findLocalProjectMetadata(
|
|
250
275
|
path.resolve(options.dir || process.cwd()),
|
|
@@ -259,6 +284,10 @@ export async function pull(options) {
|
|
|
259
284
|
auth,
|
|
260
285
|
buildId: requestedBuildId,
|
|
261
286
|
});
|
|
287
|
+
if (options.pullVersion > 0) {
|
|
288
|
+
await pullVersionBuildFiles({ options, auth, build: selectedBuild });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
262
291
|
if (options.pullMain) {
|
|
263
292
|
await pullMainBuildFiles({ options, auth, build: selectedBuild });
|
|
264
293
|
return;
|
|
@@ -676,6 +705,18 @@ export function shouldUseContributionBranch(build) {
|
|
|
676
705
|
);
|
|
677
706
|
}
|
|
678
707
|
|
|
708
|
+
// A canonical team build the viewer contributes to: plain pull/restore would
|
|
709
|
+
// switch to their contribution branch, so version-history follow-up commands
|
|
710
|
+
// for THIS build's history need --main. An owner's canonical build is their
|
|
711
|
+
// editable workspace and never needs it.
|
|
712
|
+
export function isCollaboratorMainBuild(build) {
|
|
713
|
+
return (
|
|
714
|
+
build?.role === "collaborator" &&
|
|
715
|
+
build.canWrite !== true &&
|
|
716
|
+
!Number(build?.contributionRootBuildId || 0)
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
679
720
|
export async function ensureDefaultContributionBranch({ options, auth, build }) {
|
|
680
721
|
const rootBuildId =
|
|
681
722
|
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
@@ -914,6 +955,269 @@ export async function pullMainBuildFiles({ options, auth, build }) {
|
|
|
914
955
|
);
|
|
915
956
|
}
|
|
916
957
|
|
|
958
|
+
// Read-only checkout of ONE previous save (`lumine pull --version <n>`).
|
|
959
|
+
// Without --main it targets the build the workspace edits (your branch on team
|
|
960
|
+
// projects); with --main it targets the team project's main history.
|
|
961
|
+
export async function pullVersionBuildFiles({ options, auth, build }) {
|
|
962
|
+
const versionNumber = options.pullVersion;
|
|
963
|
+
let targetBuild = build;
|
|
964
|
+
if (options.pullMain) {
|
|
965
|
+
const rootBuildId =
|
|
966
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
967
|
+
targetBuild =
|
|
968
|
+
rootBuildId === Number(build?.id || 0)
|
|
969
|
+
? build
|
|
970
|
+
: await loadBuildMetadata({ options, auth, buildId: rootBuildId });
|
|
971
|
+
} else {
|
|
972
|
+
targetBuild = await resolveEditableWorkspaceBuild({
|
|
973
|
+
options,
|
|
974
|
+
auth,
|
|
975
|
+
build,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const buildId = Number(targetBuild?.id || 0);
|
|
979
|
+
const result = await loadBuildVersionFiles({
|
|
980
|
+
options,
|
|
981
|
+
auth,
|
|
982
|
+
buildId,
|
|
983
|
+
versionNumber,
|
|
984
|
+
});
|
|
985
|
+
const resolvedBuild = result.build || targetBuild;
|
|
986
|
+
const version = result.version || { version: versionNumber };
|
|
987
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
988
|
+
// Same nesting rules as a main checkout: never create a version checkout
|
|
989
|
+
// inside another Lumine workspace; re-pulling the same checkout refreshes it
|
|
990
|
+
// in place.
|
|
991
|
+
const enclosing = options.dir
|
|
992
|
+
? null
|
|
993
|
+
: await findLocalProjectMetadata(process.cwd());
|
|
994
|
+
const enclosingIsThisCheckout =
|
|
995
|
+
enclosing?.metadata?.versionCheckout === true &&
|
|
996
|
+
Number(enclosing.metadata.buildId || 0) === buildId &&
|
|
997
|
+
Number(enclosing.metadata.checkoutVersion || 0) ===
|
|
998
|
+
Number(version.version || 0);
|
|
999
|
+
const dirName = defaultVersionCheckoutDir(resolvedBuild, version.version);
|
|
1000
|
+
const dir = path.resolve(
|
|
1001
|
+
options.dir ||
|
|
1002
|
+
(enclosingIsThisCheckout
|
|
1003
|
+
? enclosing.rootDir
|
|
1004
|
+
: enclosing
|
|
1005
|
+
? path.join(path.dirname(enclosing.rootDir), dirName)
|
|
1006
|
+
: dirName),
|
|
1007
|
+
);
|
|
1008
|
+
await writeProjectFiles({ dir, files });
|
|
1009
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1010
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
1011
|
+
}
|
|
1012
|
+
await writeInstructionFiles({
|
|
1013
|
+
dir,
|
|
1014
|
+
marker: LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
1015
|
+
content: LUMINE_VERSION_CHECKOUT_INSTRUCTIONS,
|
|
1016
|
+
});
|
|
1017
|
+
await writeSdkReference({ dir });
|
|
1018
|
+
await writeVersionCheckoutMetadata({
|
|
1019
|
+
dir,
|
|
1020
|
+
options,
|
|
1021
|
+
build: resolvedBuild,
|
|
1022
|
+
manifest: result.projectManifest || null,
|
|
1023
|
+
version,
|
|
1024
|
+
pulledAt: new Date().toISOString(),
|
|
1025
|
+
});
|
|
1026
|
+
const summaryText = version.summary ? ` — "${version.summary}"` : "";
|
|
1027
|
+
console.log(
|
|
1028
|
+
`Pulled save v${version.version} of ${formatBuildTitle(resolvedBuild)} (read-only)${summaryText}`,
|
|
1029
|
+
);
|
|
1030
|
+
console.log(
|
|
1031
|
+
`Pulled ${files.length} file${files.length === 1 ? "" : "s"} to ${dir}`,
|
|
1032
|
+
);
|
|
1033
|
+
console.log(
|
|
1034
|
+
isCollaboratorMainBuild(resolvedBuild)
|
|
1035
|
+
? `Restore it with \`lumine restore ${version.version} --main\` from your branch workspace, then \`lumine save\`.`
|
|
1036
|
+
: `Restore it with \`lumine restore ${version.version}\` from Build ${buildId}'s editable workspace, then \`lumine save\`.`,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// List a build's previous saves (every workspace/CLI save creates one).
|
|
1041
|
+
// Targets the SAME build `lumine pull --version <n>` would snapshot, so the
|
|
1042
|
+
// listed numbers and the printed follow-up commands always agree: without
|
|
1043
|
+
// --main that's the editable workspace build (a collaborator's branch on team
|
|
1044
|
+
// projects), with --main the team project's main.
|
|
1045
|
+
export async function versionsCommand(options) {
|
|
1046
|
+
const auth = await resolveAuth(options);
|
|
1047
|
+
const localProject = await findLocalProjectMetadata(
|
|
1048
|
+
path.resolve(options.dir || process.cwd()),
|
|
1049
|
+
);
|
|
1050
|
+
const requestedBuildId = options.buildIdFlag
|
|
1051
|
+
? resolveRequiredBuildId(options.buildIdFlag)
|
|
1052
|
+
: await resolveRequiredBuildIdOrSelected(options, auth, { localProject });
|
|
1053
|
+
const requestedBuild = await loadBuildMetadata({
|
|
1054
|
+
options,
|
|
1055
|
+
auth,
|
|
1056
|
+
buildId: requestedBuildId,
|
|
1057
|
+
});
|
|
1058
|
+
// Standing in a read-only checkout (pull --main / pull --version), the
|
|
1059
|
+
// resolved id IS the build being looked at — don't bounce it to the
|
|
1060
|
+
// viewer's contribution branch.
|
|
1061
|
+
const resolvedFromReadOnlyCheckout =
|
|
1062
|
+
!options.buildIdFlag &&
|
|
1063
|
+
!(resolveBuildReference(options.target).buildId > 0) &&
|
|
1064
|
+
(localProject?.metadata?.mainCheckout === true ||
|
|
1065
|
+
localProject?.metadata?.versionCheckout === true);
|
|
1066
|
+
let buildId = requestedBuildId;
|
|
1067
|
+
if (options.pullMain) {
|
|
1068
|
+
buildId =
|
|
1069
|
+
Number(requestedBuild?.contributionRootBuildId || 0) || requestedBuildId;
|
|
1070
|
+
} else if (!resolvedFromReadOnlyCheckout) {
|
|
1071
|
+
const editableBuild = await resolveEditableWorkspaceBuild({
|
|
1072
|
+
options,
|
|
1073
|
+
auth,
|
|
1074
|
+
build: requestedBuild,
|
|
1075
|
+
});
|
|
1076
|
+
buildId = Number(editableBuild?.id || 0) || requestedBuildId;
|
|
1077
|
+
}
|
|
1078
|
+
const result = await loadBuildVersions({
|
|
1079
|
+
options,
|
|
1080
|
+
auth,
|
|
1081
|
+
buildId,
|
|
1082
|
+
limit: options.limit,
|
|
1083
|
+
});
|
|
1084
|
+
printVersionList({ result });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export function printVersionList({ result }) {
|
|
1088
|
+
const build = result.build || {};
|
|
1089
|
+
const versions = Array.isArray(result.versions) ? result.versions : [];
|
|
1090
|
+
if (!versions.length) {
|
|
1091
|
+
console.log(`No previous saves found for ${formatBuildTitle(build)}.`);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
console.log(
|
|
1095
|
+
`Previous saves for ${formatBuildTitle(build)} (newest first):`,
|
|
1096
|
+
);
|
|
1097
|
+
for (const version of versions) {
|
|
1098
|
+
const author =
|
|
1099
|
+
version.createdByUsername ||
|
|
1100
|
+
(version.createdByRole === "assistant" ? "Lumine" : null) ||
|
|
1101
|
+
version.createdByRole ||
|
|
1102
|
+
"unknown";
|
|
1103
|
+
const fileText =
|
|
1104
|
+
Number(version.fileCount || 0) > 0
|
|
1105
|
+
? `${version.fileCount} file${Number(version.fileCount) === 1 ? "" : "s"}`
|
|
1106
|
+
: "single-file save";
|
|
1107
|
+
const summaryText = version.summary ? ` — ${version.summary}` : "";
|
|
1108
|
+
console.log(
|
|
1109
|
+
` v${version.version} ${formatVersionTimestamp(version.createdAt)} by ${author} (${fileText})${summaryText}`,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
// The follow-up commands must hit exactly the history that was just listed,
|
|
1113
|
+
// regardless of where the user runs them or which flags produced this list.
|
|
1114
|
+
// Embedding the listed build id makes pull workspace-independent; --main is
|
|
1115
|
+
// needed only when the listed build is a canonical team build the viewer
|
|
1116
|
+
// contributes to (plain pull/restore would switch to their branch — mirrors
|
|
1117
|
+
// shouldUseContributionBranch). An owner's canonical build never needs
|
|
1118
|
+
// --main: it IS their editable workspace.
|
|
1119
|
+
const listedBuildId = Number(build.id || 0);
|
|
1120
|
+
const collaboratorMain = isCollaboratorMainBuild(build);
|
|
1121
|
+
const pullTarget = listedBuildId ? ` ${listedBuildId}` : "";
|
|
1122
|
+
console.log(
|
|
1123
|
+
`View one read-only: lumine pull${pullTarget} --version <n>${collaboratorMain ? " --main" : ""}`,
|
|
1124
|
+
);
|
|
1125
|
+
console.log(
|
|
1126
|
+
collaboratorMain
|
|
1127
|
+
? `Bring one back: lumine restore <n> --main (run from your branch workspace; then lumine save)`
|
|
1128
|
+
: `Bring one back: lumine restore <n> (run from ${listedBuildId ? `Build ${listedBuildId}'s` : "the build's"} editable workspace; then lumine save)`,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function formatVersionTimestamp(seconds) {
|
|
1133
|
+
const timestamp = Number(seconds || 0);
|
|
1134
|
+
if (!timestamp) return "unknown time";
|
|
1135
|
+
const date = new Date(timestamp * 1000);
|
|
1136
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1137
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Write a previous save's files into the CURRENT editable workspace. Local
|
|
1141
|
+
// only on purpose: the user reviews the result and `lumine save` makes it a
|
|
1142
|
+
// new version (nothing on the server is rewritten in place). With --main in a
|
|
1143
|
+
// branch workspace, the save is read from the team project's MAIN history —
|
|
1144
|
+
// the recovery path when work was lost on main itself.
|
|
1145
|
+
export async function restoreVersion(options) {
|
|
1146
|
+
// Same strictness as `pull --version`: only a plain positive integer may
|
|
1147
|
+
// reach the workspace-overwriting path below. "2.5" must error, not
|
|
1148
|
+
// silently restore v2.
|
|
1149
|
+
const rawInput = String(options.positional[0] ?? "").trim();
|
|
1150
|
+
let versionNumber = 0;
|
|
1151
|
+
if (rawInput) {
|
|
1152
|
+
if (!/^\d+$/.test(rawInput)) {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
"Pass a plain save number, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
versionNumber = parseInt(rawInput, 10);
|
|
1158
|
+
} else if (options.pullVersion > 0) {
|
|
1159
|
+
// --version <n>, already strict-parsed by parseArgs.
|
|
1160
|
+
versionNumber = options.pullVersion;
|
|
1161
|
+
} else if (options.versionProvided) {
|
|
1162
|
+
throw new Error(
|
|
1163
|
+
"Pass a plain save number, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
if (!(versionNumber > 0)) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Pass the save number to restore, e.g. `lumine restore 41` (see `lumine versions`).",
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const auth = await resolveAuth(options);
|
|
1172
|
+
const localProject = await findLocalProjectMetadata(
|
|
1173
|
+
path.resolve(options.dir || process.cwd()),
|
|
1174
|
+
);
|
|
1175
|
+
if (!Number(localProject?.metadata?.buildId || 0)) {
|
|
1176
|
+
throw new Error(
|
|
1177
|
+
"Run `lumine restore` inside a pulled Lumine workspace (or pass --dir <workspace>).",
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
assertLocalProjectCanBeSaved(localProject);
|
|
1181
|
+
const workspaceBuildId = Number(localProject.metadata.buildId);
|
|
1182
|
+
let sourceBuildId = workspaceBuildId;
|
|
1183
|
+
if (options.pullMain) {
|
|
1184
|
+
const rootBuildId = Number(
|
|
1185
|
+
localProject.metadata.build?.contributionRootBuildId || 0,
|
|
1186
|
+
);
|
|
1187
|
+
if (!rootBuildId) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
"--main restores from the team project's main history, which needs a contribution-branch workspace.",
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
sourceBuildId = rootBuildId;
|
|
1193
|
+
}
|
|
1194
|
+
const result = await loadBuildVersionFiles({
|
|
1195
|
+
options,
|
|
1196
|
+
auth,
|
|
1197
|
+
buildId: sourceBuildId,
|
|
1198
|
+
versionNumber,
|
|
1199
|
+
});
|
|
1200
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
1201
|
+
if (!files.length) {
|
|
1202
|
+
throw new Error(`Save v${versionNumber} has no files to restore.`);
|
|
1203
|
+
}
|
|
1204
|
+
const dir = resolveProjectDirForSave({ options, localProject });
|
|
1205
|
+
await writeProjectFiles({ dir, files });
|
|
1206
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1207
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
1208
|
+
}
|
|
1209
|
+
const version = result.version || { version: versionNumber };
|
|
1210
|
+
const sourceBuild = result.build || { id: sourceBuildId };
|
|
1211
|
+
const summaryText = version.summary ? ` ("${version.summary}")` : "";
|
|
1212
|
+
console.log(
|
|
1213
|
+
`Restored ${files.length} file${files.length === 1 ? "" : "s"} from ${formatBuildTitle(sourceBuild)} save v${version.version}${summaryText} into ${dir}`,
|
|
1214
|
+
);
|
|
1215
|
+
console.log("Local only so far — review the files, then run:");
|
|
1216
|
+
console.log(
|
|
1217
|
+
` lumine save --summary ${shellQuote(`Restore from ${options.pullMain ? "main " : ""}v${version.version}`)}`,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
917
1221
|
export function printBuildList(builds) {
|
|
918
1222
|
if (!builds.length) {
|
|
919
1223
|
console.log("No owned or team Twinkle builds found.");
|
|
@@ -1233,6 +1537,10 @@ export function parseArgs(args) {
|
|
|
1233
1537
|
.map((scope) => scope.trim())
|
|
1234
1538
|
.filter(Boolean),
|
|
1235
1539
|
buildIdFlag: raw.build ? String(raw.build) : "",
|
|
1540
|
+
model: raw.model ? String(raw.model) : "",
|
|
1541
|
+
quality: raw.quality ? String(raw.quality) : "",
|
|
1542
|
+
assetName: raw.name ? String(raw.name) : "",
|
|
1543
|
+
out: raw.out ? String(raw.out) : "",
|
|
1236
1544
|
target: raw.url || raw.target || positional[0] || "",
|
|
1237
1545
|
title:
|
|
1238
1546
|
String(
|
|
@@ -1277,6 +1585,14 @@ export function parseArgs(args) {
|
|
|
1277
1585
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
1278
1586
|
dir: raw.dir ? String(raw.dir) : "",
|
|
1279
1587
|
pullMain: parseBoolean(raw.main, false),
|
|
1588
|
+
// Strict: only a plain positive integer counts. Anything else (missing
|
|
1589
|
+
// value, "foo", a swallowed flag like "--main", "2.5") stays 0 and the
|
|
1590
|
+
// consuming command must reject it via versionProvided instead of
|
|
1591
|
+
// silently falling back to a live pull.
|
|
1592
|
+
versionProvided: Object.prototype.hasOwnProperty.call(raw, "version"),
|
|
1593
|
+
pullVersion: /^\d+$/.test(String(raw.version ?? "").trim())
|
|
1594
|
+
? parseInt(String(raw.version).trim(), 10)
|
|
1595
|
+
: 0,
|
|
1280
1596
|
summary: raw.summary ? String(raw.summary) : "",
|
|
1281
1597
|
publish: parseBoolean(raw.publish, false),
|
|
1282
1598
|
saveFirst: parseBoolean(raw.save, false),
|
|
@@ -1335,6 +1651,22 @@ export async function resolveRequiredBuildIdOrSelected(
|
|
|
1335
1651
|
}
|
|
1336
1652
|
if (mainBuildId > 0) return mainBuildId;
|
|
1337
1653
|
}
|
|
1654
|
+
// Same deal for a `pull --version <n>` checkout: read-only commands resolve
|
|
1655
|
+
// the build it snapshots; mutating commands are pointed back at the
|
|
1656
|
+
// editable workspace.
|
|
1657
|
+
if (resolvedLocalProject?.metadata?.versionCheckout === true) {
|
|
1658
|
+
const checkoutBuildId =
|
|
1659
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1660
|
+
Number(resolvedLocalProject.metadata.build?.id || 0);
|
|
1661
|
+
const checkoutVersion =
|
|
1662
|
+
Number(resolvedLocalProject.metadata.checkoutVersion || 0) || 0;
|
|
1663
|
+
if (!MAIN_CHECKOUT_READONLY_COMMANDS.has(options.command)) {
|
|
1664
|
+
throw new Error(
|
|
1665
|
+
`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.`,
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
if (checkoutBuildId > 0) return checkoutBuildId;
|
|
1669
|
+
}
|
|
1338
1670
|
if (
|
|
1339
1671
|
resolvedLocalProject?.metadata &&
|
|
1340
1672
|
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
@@ -1424,6 +1756,9 @@ export function printHelp() {
|
|
|
1424
1756
|
lumine select [twinkle-build-url]
|
|
1425
1757
|
lumine pull [twinkle-build-url]
|
|
1426
1758
|
lumine pull [twinkle-build-url] --main
|
|
1759
|
+
lumine pull [twinkle-build-url] --version <n> [--main]
|
|
1760
|
+
lumine versions [twinkle-build-url] [--main] [--limit <n>]
|
|
1761
|
+
lumine restore <n> [--main]
|
|
1427
1762
|
lumine reference <twinkle-build-url>
|
|
1428
1763
|
lumine fork <twinkle-build-url>
|
|
1429
1764
|
lumine diff <twinkle-branch-url>
|
|
@@ -1437,8 +1772,12 @@ export function printHelp() {
|
|
|
1437
1772
|
lumine sdk call <namespace.method> [jsonArgs]
|
|
1438
1773
|
lumine assets [list]
|
|
1439
1774
|
lumine assets upload <file...>
|
|
1775
|
+
lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>
|
|
1440
1776
|
lumine assets delete <assetId>
|
|
1441
1777
|
lumine assets prune [--yes]
|
|
1778
|
+
lumine thumbnail set <file>
|
|
1779
|
+
lumine thumbnail capture [--out <file>]
|
|
1780
|
+
lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
|
|
1442
1781
|
|
|
1443
1782
|
Examples:
|
|
1444
1783
|
npx @stage5/lumine@latest
|
|
@@ -1451,6 +1790,9 @@ Examples:
|
|
|
1451
1790
|
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
1452
1791
|
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
1453
1792
|
npx @stage5/lumine@latest pull 884 --main
|
|
1793
|
+
npx @stage5/lumine@latest versions --main
|
|
1794
|
+
npx @stage5/lumine@latest pull --version 41 --main
|
|
1795
|
+
npx @stage5/lumine@latest restore 41 --main
|
|
1454
1796
|
npx @stage5/lumine@latest update-from-main
|
|
1455
1797
|
npx @stage5/lumine@latest pull
|
|
1456
1798
|
npx @stage5/lumine@latest save
|
|
@@ -1462,6 +1804,10 @@ Examples:
|
|
|
1462
1804
|
npx @stage5/lumine@latest sdk call aiStories.list '{"difficulty": 1}' --repeat 5
|
|
1463
1805
|
npx @stage5/lumine@latest assets upload art/hero.png sounds/theme.mp3
|
|
1464
1806
|
npx @stage5/lumine@latest assets list --build 917
|
|
1807
|
+
npx @stage5/lumine@latest assets generate "pixel-art forest background" --model nano-banana
|
|
1808
|
+
npx @stage5/lumine@latest thumbnail set art/cover.png
|
|
1809
|
+
npx @stage5/lumine@latest thumbnail capture --out capture.png
|
|
1810
|
+
npx @stage5/lumine@latest thumbnail generate --model gpt-image-2 --yes
|
|
1465
1811
|
|
|
1466
1812
|
Options:
|
|
1467
1813
|
--api-url <url> Twinkle API origin
|
|
@@ -1469,7 +1815,8 @@ Options:
|
|
|
1469
1815
|
--auth-file <path> Saved login path
|
|
1470
1816
|
--auth-token <token> Override saved login
|
|
1471
1817
|
--dir <path> Directory for pulled project files
|
|
1472
|
-
--main With pull:
|
|
1818
|
+
--main With pull/versions/restore: target the team project's main
|
|
1819
|
+
--version <n> With pull: read-only checkout of previous save v<n>
|
|
1473
1820
|
--title <text> New Build title
|
|
1474
1821
|
--description <text> Optional New Build description
|
|
1475
1822
|
--no-description Skip the New Build description prompt
|
|
@@ -1481,11 +1828,15 @@ Options:
|
|
|
1481
1828
|
--limit <number> Number of projects to show
|
|
1482
1829
|
--no-update-check Skip the npm latest-version check
|
|
1483
1830
|
--no-open Print the approval URL without opening a browser
|
|
1484
|
-
--build <id> Build id for sdk/assets calls outside a workspace
|
|
1831
|
+
--build <id> Build id for sdk/assets/thumbnail calls outside a workspace
|
|
1485
1832
|
--repeat <n> Repeat an sdk call (1-20) and print latency stats
|
|
1486
1833
|
--allow-write Permit sdk methods that mutate app data
|
|
1487
1834
|
--path <api/...> Call an sdk endpoint not in the curated list
|
|
1488
1835
|
--scopes <a,b> Override requested build API token scopes
|
|
1489
|
-
--
|
|
1836
|
+
--model <model> Image model for generate: gpt-image-2 or nano-banana (required, no default)
|
|
1837
|
+
--quality <q> gpt-image-2 quality: low, medium, high (default high)
|
|
1838
|
+
--name <fileName> File name hint for a generated asset
|
|
1839
|
+
--out <path> With thumbnail capture: also save the capture locally
|
|
1840
|
+
--yes Skip confirmation prompts (assets prune/generate, thumbnail)
|
|
1490
1841
|
`);
|
|
1491
1842
|
}
|
package/lib/constants.js
CHANGED
|
@@ -10,6 +10,25 @@ export const DEFAULT_AUTH_FILE = path.join(
|
|
|
10
10
|
"lumine-cli-auth.json",
|
|
11
11
|
);
|
|
12
12
|
export const DEFAULT_TIMEOUT_MS = 20000;
|
|
13
|
+
// Image generation runs a multi-minute provider call; thumbnail capture runs a
|
|
14
|
+
// headless browser server-side. Both need per-call timeouts far above the
|
|
15
|
+
// 20s default.
|
|
16
|
+
export const ASSET_GENERATE_TIMEOUT_MS = 6 * 60 * 1000;
|
|
17
|
+
export const THUMBNAIL_CAPTURE_TIMEOUT_MS = 90 * 1000;
|
|
18
|
+
export const GENERATE_MODEL_ALIASES = {
|
|
19
|
+
"gpt-image-2": "gpt-image-2",
|
|
20
|
+
"nano-banana": "gemini-3-pro-image-preview",
|
|
21
|
+
"gemini-3-pro-image-preview": "gemini-3-pro-image-preview",
|
|
22
|
+
};
|
|
23
|
+
export const GENERATE_QUALITIES = new Set(["low", "medium", "high"]);
|
|
24
|
+
// Server accepts only these thumbnail content types (8MB max).
|
|
25
|
+
export const THUMBNAIL_CONTENT_TYPE_BY_EXTENSION = {
|
|
26
|
+
".jpg": "image/jpeg",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".png": "image/png",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
};
|
|
31
|
+
export const THUMBNAIL_MAX_FILE_SIZE_BYTES = 8 * 1024 * 1024;
|
|
13
32
|
export const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
14
33
|
export const DEFAULT_PROJECT_LIMIT = 50;
|
|
15
34
|
export const PROJECT_METADATA_DIR = ".twinkle";
|
|
@@ -69,6 +88,8 @@ export const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
|
69
88
|
"<!-- Lumine CLI Reference Instructions -->";
|
|
70
89
|
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
71
90
|
"<!-- Lumine CLI Main Checkout Instructions -->";
|
|
91
|
+
export const LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
92
|
+
"<!-- Lumine CLI Version Checkout Instructions -->";
|
|
72
93
|
export const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
73
94
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
74
95
|
import.meta.url,
|
|
@@ -120,6 +141,12 @@ lumine save --summary "Describe the change"
|
|
|
120
141
|
\`\`\`
|
|
121
142
|
|
|
122
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.
|
|
123
150
|
- Use \`lumine sdk call <namespace.method> '{...}'\` to inspect real endpoint
|
|
124
151
|
data and measure latency (add --repeat <n>); \`lumine sdk list\` shows
|
|
125
152
|
callable methods. It prints the raw HTTP endpoint response, which can differ
|
|
@@ -152,6 +179,26 @@ lumine save --summary "Describe the change"
|
|
|
152
179
|
replaces. Update code references, then delete or prune the old asset.
|
|
153
180
|
- Prefer one small assets module (e.g. src/assets.js) mapping names to asset
|
|
154
181
|
URLs over scattering URLs through the code.
|
|
182
|
+
- \`lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>\` creates
|
|
183
|
+
an AI-generated image asset. --model is REQUIRED (no default): gpt-image-2 is
|
|
184
|
+
best quality but slower and pricier; nano-banana (Gemini) is faster and
|
|
185
|
+
cheaper. It spends the logged-in user's AI Battery, so it asks y/N with the
|
|
186
|
+
estimated cost first; non-interactive runs must pass --yes to consent.
|
|
187
|
+
Optional: --quality low|medium|high (gpt-image-2 only, default high),
|
|
188
|
+
--name <fileName>. The asset lands in .twinkle/${ASSETS_METADATA_FILE} like an upload.
|
|
189
|
+
|
|
190
|
+
## Thumbnail
|
|
191
|
+
|
|
192
|
+
- \`lumine thumbnail set <file>\` uploads a jpg/png/webp (max 8MB) as the
|
|
193
|
+
build's thumbnail. Thumbnails display ~16:9; the image is uploaded uncropped.
|
|
194
|
+
- \`lumine thumbnail capture\` screenshots the running app server-side and sets
|
|
195
|
+
the result as the thumbnail (add --out <file> to also save it locally).
|
|
196
|
+
- \`lumine thumbnail generate "<prompt>" --model <gpt-image-2|nano-banana>\`
|
|
197
|
+
generates an AI image and sets it as the thumbnail (also stored as a normal
|
|
198
|
+
reusable asset). Same battery-cost consent rules as \`assets generate\`.
|
|
199
|
+
- Replacing an existing thumbnail asks y/N; pass --yes for non-interactive runs.
|
|
200
|
+
- Never run thumbnail commands from a read-only \`pull --main\` checkout; use
|
|
201
|
+
your contribution-branch workspace or an owned build.
|
|
155
202
|
|
|
156
203
|
## Project File Limits
|
|
157
204
|
|
|
@@ -167,6 +214,7 @@ lumine save --summary "Describe the change"
|
|
|
167
214
|
|
|
168
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.
|
|
169
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.
|
|
170
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.
|
|
171
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.
|
|
172
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.
|
|
@@ -201,6 +249,22 @@ the workspace to save.
|
|
|
201
249
|
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
202
250
|
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
203
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
|
+
`;
|
|
204
268
|
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS = `${LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER}
|
|
205
269
|
# Lumine Main Checkout (Read-Only)
|
|
206
270
|
|
|
@@ -229,6 +293,8 @@ export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
|
|
|
229
293
|
// assets never touch main's project files (uploads go to the current
|
|
230
294
|
// user's own asset space), so listing/uploading from a main checkout is safe.
|
|
231
295
|
"assets",
|
|
296
|
+
// versions only reads save history.
|
|
297
|
+
"versions",
|
|
232
298
|
]);
|
|
233
299
|
export const COMMANDS = new Set([
|
|
234
300
|
"workspace",
|
|
@@ -250,7 +316,10 @@ export const COMMANDS = new Set([
|
|
|
250
316
|
"push",
|
|
251
317
|
"check",
|
|
252
318
|
"launch",
|
|
319
|
+
"versions",
|
|
320
|
+
"restore",
|
|
253
321
|
"sdk",
|
|
254
322
|
"assets",
|
|
323
|
+
"thumbnail",
|
|
255
324
|
"help",
|
|
256
325
|
]);
|