@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.26.2
4
- Updated: 2026-06-09
5
- Generated: 2026-06-18T12:26:29.648Z
3
+ Version: 1.26.3
4
+ Updated: 2026-07-08
5
+ Generated: 2026-07-08T09:25:17.959Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -21,12 +21,28 @@ 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.
25
+ - Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.
24
26
 
25
27
  ## Token Scopes
26
28
  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
27
29
 
28
30
  ## Namespaces
29
31
 
32
+ ### Twinkle
33
+ - isAudioMuted() | scopes: none
34
+ - Returns: boolean
35
+ - Returns whether the host runtime currently has this Build app tab muted.
36
+ - The host automatically mutes standard <audio>/<video> elements and Web Audio nodes connected to AudioContext.destination.
37
+ - Use this only when your app manages audio outside those standard paths.
38
+ - Example: if (Twinkle.isAudioMuted()) pauseCustomMixerOutput();
39
+ - onAudioMuteChange(listener, options?) | scopes: none
40
+ - Returns: unsubscribe function
41
+ - Subscribe to host tab mute changes for custom audio engines.
42
+ - The listener receives the current muted boolean immediately by default.
43
+ - Pass { immediate: false } to skip the initial callback.
44
+ - Example: const unsubscribe = Twinkle.onAudioMuteChange((muted) => customMixer.setMuted(muted));
45
+
30
46
  ### Twinkle.capabilities
31
47
  - async get() | scopes: none
32
48
  - Returns: Capability snapshot
@@ -49,6 +65,22 @@ files:read, user:read, users:read, dailyReflections:read, content:read, sharedDb
49
65
  - Returns: Viewer info
50
66
  - Forces a fresh fetch from the parent.
51
67
 
68
+ ### Twinkle.app
69
+ - async getInfo() | scopes: none
70
+ - Returns: App info object from the parent (includes appUrl) or null
71
+ - Cached after the first call for the iframe session.
72
+ - async getShareUrl(pathSegment) | scopes: none
73
+ - Returns: Canonical shareable deep-link URL string, or null when app info is unavailable
74
+ - Builds a canonical shareable deep link into this app, e.g. https://www.twin-kle.com/app/884/432-the-great-gatsby.
75
+ - Example: await Twinkle.app.getShareUrl('432-the-great-gatsby');
76
+ - async navigate(target) | scopes: none
77
+ - Returns: { success, src }
78
+ - Navigate to another Build preview route through the parent bridge without dropping Twinkle SDK access.
79
+ - Use this for in-app Build preview/world switches instead of window.location.assign, location.replace, or setting location.href.
80
+ - The parent validates that the target is still a Build preview URL before navigating.
81
+ - External URLs are rejected and do not receive the Build bridge nonce.
82
+ - Example: await Twinkle.app.navigate('./arena.html');
83
+
52
84
  ### Twinkle.preview
53
85
  - getLayout() | scopes: none
54
86
  - Returns: { mode, viewport, stage, safeInsets, playfield }