@stage5/lumine 0.2.6 → 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/lib/api.js +23 -0
- package/lib/commands.js +331 -1
- package/lib/constants.js +29 -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/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,
|
|
@@ -53,10 +57,12 @@ import { sdkCommand } from "./sdk.js";
|
|
|
53
57
|
import {
|
|
54
58
|
defaultMainCheckoutDir,
|
|
55
59
|
defaultReferenceDir,
|
|
60
|
+
defaultVersionCheckoutDir,
|
|
56
61
|
defaultWorkspaceDir,
|
|
57
62
|
parseBoolean,
|
|
58
63
|
resolveBuildId,
|
|
59
64
|
resolveBuildReference,
|
|
65
|
+
resolveRequiredBuildId,
|
|
60
66
|
shellQuote,
|
|
61
67
|
toCamelCase,
|
|
62
68
|
trimTrailingSlash,
|
|
@@ -75,6 +81,7 @@ import {
|
|
|
75
81
|
writeInstructionFiles,
|
|
76
82
|
writeMainCheckoutMetadata,
|
|
77
83
|
writeProjectFiles,
|
|
84
|
+
writeVersionCheckoutMetadata,
|
|
78
85
|
writeProjectMetadata,
|
|
79
86
|
writeReferenceInstructions,
|
|
80
87
|
writeReferenceMetadata,
|
|
@@ -160,6 +167,14 @@ export async function main() {
|
|
|
160
167
|
await check(options);
|
|
161
168
|
return;
|
|
162
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
|
+
}
|
|
163
178
|
if (options.command === "launch") {
|
|
164
179
|
await launch(options);
|
|
165
180
|
return;
|
|
@@ -250,6 +265,11 @@ export async function selectProject(options) {
|
|
|
250
265
|
}
|
|
251
266
|
|
|
252
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
|
+
}
|
|
253
273
|
const auth = await resolveAuth(options);
|
|
254
274
|
const localProject = await findLocalProjectMetadata(
|
|
255
275
|
path.resolve(options.dir || process.cwd()),
|
|
@@ -264,6 +284,10 @@ export async function pull(options) {
|
|
|
264
284
|
auth,
|
|
265
285
|
buildId: requestedBuildId,
|
|
266
286
|
});
|
|
287
|
+
if (options.pullVersion > 0) {
|
|
288
|
+
await pullVersionBuildFiles({ options, auth, build: selectedBuild });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
267
291
|
if (options.pullMain) {
|
|
268
292
|
await pullMainBuildFiles({ options, auth, build: selectedBuild });
|
|
269
293
|
return;
|
|
@@ -681,6 +705,18 @@ export function shouldUseContributionBranch(build) {
|
|
|
681
705
|
);
|
|
682
706
|
}
|
|
683
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
|
+
|
|
684
720
|
export async function ensureDefaultContributionBranch({ options, auth, build }) {
|
|
685
721
|
const rootBuildId =
|
|
686
722
|
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
@@ -919,6 +955,269 @@ export async function pullMainBuildFiles({ options, auth, build }) {
|
|
|
919
955
|
);
|
|
920
956
|
}
|
|
921
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
|
+
|
|
922
1221
|
export function printBuildList(builds) {
|
|
923
1222
|
if (!builds.length) {
|
|
924
1223
|
console.log("No owned or team Twinkle builds found.");
|
|
@@ -1286,6 +1585,14 @@ export function parseArgs(args) {
|
|
|
1286
1585
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
1287
1586
|
dir: raw.dir ? String(raw.dir) : "",
|
|
1288
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,
|
|
1289
1596
|
summary: raw.summary ? String(raw.summary) : "",
|
|
1290
1597
|
publish: parseBoolean(raw.publish, false),
|
|
1291
1598
|
saveFirst: parseBoolean(raw.save, false),
|
|
@@ -1344,6 +1651,22 @@ export async function resolveRequiredBuildIdOrSelected(
|
|
|
1344
1651
|
}
|
|
1345
1652
|
if (mainBuildId > 0) return mainBuildId;
|
|
1346
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
|
+
}
|
|
1347
1670
|
if (
|
|
1348
1671
|
resolvedLocalProject?.metadata &&
|
|
1349
1672
|
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
@@ -1433,6 +1756,9 @@ export function printHelp() {
|
|
|
1433
1756
|
lumine select [twinkle-build-url]
|
|
1434
1757
|
lumine pull [twinkle-build-url]
|
|
1435
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]
|
|
1436
1762
|
lumine reference <twinkle-build-url>
|
|
1437
1763
|
lumine fork <twinkle-build-url>
|
|
1438
1764
|
lumine diff <twinkle-branch-url>
|
|
@@ -1464,6 +1790,9 @@ Examples:
|
|
|
1464
1790
|
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
1465
1791
|
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
1466
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
|
|
1467
1796
|
npx @stage5/lumine@latest update-from-main
|
|
1468
1797
|
npx @stage5/lumine@latest pull
|
|
1469
1798
|
npx @stage5/lumine@latest save
|
|
@@ -1486,7 +1815,8 @@ Options:
|
|
|
1486
1815
|
--auth-file <path> Saved login path
|
|
1487
1816
|
--auth-token <token> Override saved login
|
|
1488
1817
|
--dir <path> Directory for pulled project files
|
|
1489
|
-
--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>
|
|
1490
1820
|
--title <text> New Build title
|
|
1491
1821
|
--description <text> Optional New Build description
|
|
1492
1822
|
--no-description Skip the New Build description prompt
|
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,6 +316,8 @@ 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",
|
package/lib/util.js
CHANGED
|
@@ -86,6 +86,13 @@ export function defaultMainCheckoutDir(build) {
|
|
|
86
86
|
return `twinkle-main-${titleSlug || "build"}-${buildId}`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export function defaultVersionCheckoutDir(build, versionNumber) {
|
|
90
|
+
const titleSlug = slugify(build?.title || "");
|
|
91
|
+
const buildId = Number(build?.id || 0) || "build";
|
|
92
|
+
const version = Number(versionNumber || 0) || "version";
|
|
93
|
+
return `twinkle-v${version}-${titleSlug || "build"}-${buildId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
export function resolveRequiredBuildId(value) {
|
|
90
97
|
const buildId = resolveBuildReference(value).buildId;
|
|
91
98
|
if (buildId > 0) return buildId;
|
package/lib/workspace.js
CHANGED
|
@@ -491,6 +491,54 @@ export async function writeMainCheckoutMetadata({
|
|
|
491
491
|
);
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
// Metadata for a read-only `pull --version <n>` checkout of one previous save:
|
|
495
|
+
// versionCheckout marks it so save can point back at the restore flow.
|
|
496
|
+
export async function writeVersionCheckoutMetadata({
|
|
497
|
+
dir,
|
|
498
|
+
options,
|
|
499
|
+
build,
|
|
500
|
+
manifest,
|
|
501
|
+
version,
|
|
502
|
+
pulledAt,
|
|
503
|
+
}) {
|
|
504
|
+
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
505
|
+
await fs.mkdir(metadataDir, { recursive: true });
|
|
506
|
+
const buildId = Number(build?.id || 0) || null;
|
|
507
|
+
await fs.writeFile(
|
|
508
|
+
path.join(metadataDir, PROJECT_METADATA_FILE),
|
|
509
|
+
JSON.stringify(
|
|
510
|
+
{
|
|
511
|
+
schemaVersion: 1,
|
|
512
|
+
buildId,
|
|
513
|
+
readOnly: true,
|
|
514
|
+
versionCheckout: true,
|
|
515
|
+
checkoutVersion: Number(version?.version || 0) || null,
|
|
516
|
+
checkoutVersionSummary: version?.summary || null,
|
|
517
|
+
checkoutVersionCreatedAt: Number(version?.createdAt || 0) || null,
|
|
518
|
+
build: {
|
|
519
|
+
id: buildId,
|
|
520
|
+
title: build?.title || (buildId ? `Build ${buildId}` : ""),
|
|
521
|
+
role: build?.role || "collaborator",
|
|
522
|
+
ownerUsername: build?.ownerUsername || null,
|
|
523
|
+
contributionStatus: "none",
|
|
524
|
+
contributionRootBuildId:
|
|
525
|
+
Number(build?.contributionRootBuildId || 0) || null,
|
|
526
|
+
canWrite: false,
|
|
527
|
+
canPublish: false,
|
|
528
|
+
},
|
|
529
|
+
apiUrl: options.apiUrl,
|
|
530
|
+
siteUrl: options.siteUrl,
|
|
531
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
532
|
+
manifest,
|
|
533
|
+
pulledAt,
|
|
534
|
+
},
|
|
535
|
+
null,
|
|
536
|
+
2,
|
|
537
|
+
),
|
|
538
|
+
"utf8",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
494
542
|
export async function findLocalProjectMetadata(startDir) {
|
|
495
543
|
let current = path.resolve(startDir || process.cwd());
|
|
496
544
|
while (true) {
|
|
@@ -527,6 +575,14 @@ export function assertLocalProjectCanBeSaved(localProject) {
|
|
|
527
575
|
`This is a read-only checkout of main${rootBuildId ? ` for Build ${rootBuildId}` : ""}. Make edits in your branch workspace (\`lumine pull${rootBuildId ? ` ${rootBuildId}` : ""}\`), and run \`lumine update-from-main\` there to bring main's changes into it.`,
|
|
528
576
|
);
|
|
529
577
|
}
|
|
578
|
+
if (metadata.versionCheckout === true) {
|
|
579
|
+
const checkoutBuildId =
|
|
580
|
+
Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
|
|
581
|
+
const checkoutVersion = Number(metadata.checkoutVersion || 0) || 0;
|
|
582
|
+
throw new Error(
|
|
583
|
+
`This is a read-only checkout of a previous save${checkoutVersion ? ` (v${checkoutVersion})` : ""}${checkoutBuildId ? ` for Build ${checkoutBuildId}` : ""}. To bring this save back, run \`lumine restore${checkoutVersion ? ` ${checkoutVersion}` : " <n>"}\` from the editable workspace, then \`lumine save\`.`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
530
586
|
if (isReadOnlyReferenceMetadata(metadata)) {
|
|
531
587
|
const sourceBuildId =
|
|
532
588
|
Number(metadata.reference?.sourceBuildId || 0) ||
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
3
|
Version: 1.26.2
|
|
4
|
-
Updated: 2026-
|
|
5
|
-
Generated: 2026-
|
|
4
|
+
Updated: 2026-07-05
|
|
5
|
+
Generated: 2026-07-05T06:47:32.349Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -21,6 +21,7 @@ Generated: 2026-06-18T12:26:29.648Z
|
|
|
21
21
|
- Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; world sessions are disposable and durable MMO state belongs in sharedDb/privateDb.
|
|
22
22
|
- Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
|
|
23
23
|
- Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
|
|
24
|
+
- Interface text must not be selectable on touch devices: apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls) so mobile long-press does not highlight UI. Keep text inputs and genuinely user-copyable content selectable.
|
|
24
25
|
|
|
25
26
|
## Token Scopes
|
|
26
27
|
files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write
|
|
@@ -49,6 +50,22 @@ files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb
|
|
|
49
50
|
- Returns: Viewer info
|
|
50
51
|
- Forces a fresh fetch from the parent.
|
|
51
52
|
|
|
53
|
+
### Twinkle.app
|
|
54
|
+
- async getInfo() | scopes: none
|
|
55
|
+
- Returns: App info object from the parent (includes appUrl) or null
|
|
56
|
+
- Cached after the first call for the iframe session.
|
|
57
|
+
- async getShareUrl(pathSegment) | scopes: none
|
|
58
|
+
- Returns: Canonical shareable deep-link URL string, or null when app info is unavailable
|
|
59
|
+
- Builds a canonical shareable deep link into this app, e.g. https://www.twin-kle.com/app/884/432-the-great-gatsby.
|
|
60
|
+
- Example: await Twinkle.app.getShareUrl('432-the-great-gatsby');
|
|
61
|
+
- async navigate(target) | scopes: none
|
|
62
|
+
- Returns: { success, src }
|
|
63
|
+
- Navigate to another Build preview route through the parent bridge without dropping Twinkle SDK access.
|
|
64
|
+
- Use this for in-app Build preview/world switches instead of window.location.assign, location.replace, or setting location.href.
|
|
65
|
+
- The parent validates that the target is still a Build preview URL before navigating.
|
|
66
|
+
- External URLs are rejected and do not receive the Build bridge nonce.
|
|
67
|
+
- Example: await Twinkle.app.navigate('./arena.html');
|
|
68
|
+
|
|
52
69
|
### Twinkle.preview
|
|
53
70
|
- getLayout() | scopes: none
|
|
54
71
|
- Returns: { mode, viewport, stage, safeInsets, playfield }
|