@stage5/lumine 0.2.3 → 0.2.4
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/bin/lumine.js +332 -15
- package/package.json +1 -1
package/bin/lumine.js
CHANGED
|
@@ -33,6 +33,8 @@ const LUMINE_AGENT_INSTRUCTIONS_MARKER =
|
|
|
33
33
|
const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
|
|
34
34
|
const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
35
35
|
"<!-- Lumine CLI Reference Instructions -->";
|
|
36
|
+
const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
37
|
+
"<!-- Lumine CLI Main Checkout Instructions -->";
|
|
36
38
|
const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
37
39
|
"../sdk/BUILD_SDK_INDEX.md",
|
|
38
40
|
import.meta.url,
|
|
@@ -70,6 +72,7 @@ Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
|
70
72
|
- Read ${SDK_REFERENCE_FILE} before adding, removing, or changing any Twinkle.* SDK calls.
|
|
71
73
|
- If build.canWrite is false, do not save changes.
|
|
72
74
|
- If build.canPublish is false or contributionRootBuildId is set, this checkout is a contribution branch. Save only to this branch and do not run lumine launch or lumine save --publish.
|
|
75
|
+
- On a contribution branch, main may have moved since the branch was created. Before starting large edits, run \`lumine update-from-main\` to three-way-merge main into the branch (resolve any <<<<<<< conflict markers it reports, then save). \`lumine pull --main\` gives a read-only checkout of main for comparison (created alongside your workspace, never inside it).
|
|
73
76
|
- Do not edit another local checkout to bypass branch rules.
|
|
74
77
|
|
|
75
78
|
## Workflow
|
|
@@ -100,6 +103,14 @@ lumine save --summary "Describe the change"
|
|
|
100
103
|
- Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
|
|
101
104
|
- Match storage to update frequency. Twinkle.privateDb and Twinkle.sharedDb are for LOW-frequency durable state only — things that change on a user action (settings, inventory checkpoints, completed quests, saved progress; comments, votes, room settings, submitted records). NEVER write high-frequency or per-frame/per-tick state to them (camera or cursor position, animation state, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and for durable per-user state flush an occasional snapshot on an interval or on exit (never per frame) — e.g. the viewer/user DB or a single latest-snapshot key. The server rate-limits these writes per key and returns 429 on excess; never retry-loop a 429.
|
|
102
105
|
|
|
106
|
+
## Local Testing (Playwright / browser probes)
|
|
107
|
+
|
|
108
|
+
- Serve the workspace with a tiny local HTTP server and drive it with Playwright. NEVER copy probe/vendor files into the workspace dir — lumine save uploads everything here (and binary files fail validation). Build a sibling probe dir that symlinks the workspace files instead.
|
|
109
|
+
- Vendored imports like /build/vendor/three/0.184.0/... are absolute paths: mirror that directory under your probe dir's root and fetch the files from the LIVE SITE (e.g. https://www.twin-kle.com/build/vendor/three/0.184.0/three.webgpu.min.js). Do NOT use npm/CDN copies — the platform's vendored builds have rewritten import specifiers (npm three.tsl.min.js still imports bare "three/webgpu" and breaks the module graph).
|
|
110
|
+
- The three WebGPU renderer falls back to WebGL2 in headless Chromium automatically. Headless software rendering runs at ~2-5fps, so anything time-based (walking a character, timers) takes ~10-20x longer than real time — loop with generous waits instead of fixed short sleeps, and bump navigation timeouts.
|
|
111
|
+
- To inspect module-scope game state, append debug getters when SERVING main.js (e.g. body += "window.__dbg = () => ({...})") rather than editing workspace files.
|
|
112
|
+
- SDK calls are absent when serving locally; well-written builds optional-chain window.Twinkle and fall back to localStorage. Seed localStorage in the probe to fake saves.
|
|
113
|
+
|
|
103
114
|
## Completion Report
|
|
104
115
|
|
|
105
116
|
Report the changed files, any SDK methods used, the lumine save result, the
|
|
@@ -120,7 +131,32 @@ the workspace to save.
|
|
|
120
131
|
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
121
132
|
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
122
133
|
`;
|
|
134
|
+
const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS = `${LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER}
|
|
135
|
+
# Lumine Main Checkout (Read-Only)
|
|
136
|
+
|
|
137
|
+
This directory is a read-only snapshot of a team project's MAIN workspace,
|
|
138
|
+
pulled with \`lumine pull --main\`. Use it to inspect what main currently looks
|
|
139
|
+
like; it is not the workspace to edit or save.
|
|
140
|
+
|
|
141
|
+
## Source Of Truth
|
|
142
|
+
|
|
143
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
144
|
+
- metadata.mainCheckout is true here: do not run lumine save from this directory.
|
|
145
|
+
- Make changes in your contribution-branch workspace (lumine pull <buildId>).
|
|
146
|
+
- Bring main's latest changes into your branch with lumine update-from-main.
|
|
147
|
+
- Do not edit another local checkout to bypass read-only semantics.
|
|
148
|
+
`;
|
|
123
149
|
const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
150
|
+
// Commands allowed to resolve a read-only `pull --main` checkout's build id.
|
|
151
|
+
// Everything else (launch/save/merge/…) mutates and must not run from one.
|
|
152
|
+
const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
|
|
153
|
+
"pull",
|
|
154
|
+
"check",
|
|
155
|
+
"diff",
|
|
156
|
+
"sdk",
|
|
157
|
+
"select",
|
|
158
|
+
"workspace",
|
|
159
|
+
]);
|
|
124
160
|
const COMMANDS = new Set([
|
|
125
161
|
"workspace",
|
|
126
162
|
"login",
|
|
@@ -136,6 +172,7 @@ const COMMANDS = new Set([
|
|
|
136
172
|
"diff",
|
|
137
173
|
"merge",
|
|
138
174
|
"replace-main",
|
|
175
|
+
"update-from-main",
|
|
139
176
|
"save",
|
|
140
177
|
"push",
|
|
141
178
|
"check",
|
|
@@ -216,6 +253,10 @@ async function main() {
|
|
|
216
253
|
await replaceMainWithBranch(options);
|
|
217
254
|
return;
|
|
218
255
|
}
|
|
256
|
+
if (options.command === "update-from-main") {
|
|
257
|
+
await updateBranchFromMain(options);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
219
260
|
if (options.command === "save" || options.command === "push") {
|
|
220
261
|
await save(options);
|
|
221
262
|
return;
|
|
@@ -408,12 +449,23 @@ async function selectProject(options) {
|
|
|
408
449
|
|
|
409
450
|
async function pull(options) {
|
|
410
451
|
const auth = await resolveAuth(options);
|
|
411
|
-
const
|
|
452
|
+
const localProject = await findLocalProjectMetadata(
|
|
453
|
+
path.resolve(options.dir || process.cwd()),
|
|
454
|
+
);
|
|
455
|
+
const requestedBuildId = await resolveRequiredBuildIdOrSelected(
|
|
456
|
+
options,
|
|
457
|
+
auth,
|
|
458
|
+
options.pullMain ? { localProject } : {},
|
|
459
|
+
);
|
|
412
460
|
const selectedBuild = await loadBuildMetadata({
|
|
413
461
|
options,
|
|
414
462
|
auth,
|
|
415
463
|
buildId: requestedBuildId,
|
|
416
464
|
});
|
|
465
|
+
if (options.pullMain) {
|
|
466
|
+
await pullMainBuildFiles({ options, auth, build: selectedBuild });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
417
469
|
const build = await resolveEditableWorkspaceBuild({
|
|
418
470
|
options,
|
|
419
471
|
auth,
|
|
@@ -424,6 +476,65 @@ async function pull(options) {
|
|
|
424
476
|
printPullResult(result);
|
|
425
477
|
}
|
|
426
478
|
|
|
479
|
+
// Sync a contribution branch with its team project's main: the server runs a
|
|
480
|
+
// three-way merge (auto-merging where it can, writing git-style conflict
|
|
481
|
+
// markers where it cannot) and saves the result to the branch. When run inside
|
|
482
|
+
// the branch's workspace, local edits are sent along as the branch's pending
|
|
483
|
+
// state and the merged files are written back to disk.
|
|
484
|
+
async function updateBranchFromMain(options) {
|
|
485
|
+
const auth = await resolveAuth(options);
|
|
486
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
487
|
+
const localProject = await findLocalProjectMetadata(
|
|
488
|
+
path.resolve(options.dir || process.cwd()),
|
|
489
|
+
);
|
|
490
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
491
|
+
localProject,
|
|
492
|
+
});
|
|
493
|
+
const build = await loadBuildMetadata({ options, auth, buildId });
|
|
494
|
+
const rootBuildId = Number(build?.contributionRootBuildId || 0);
|
|
495
|
+
const contributionBuildId = Number(build?.id || 0);
|
|
496
|
+
if (!rootBuildId || !contributionBuildId) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"update-from-main only applies to contribution branches of a team project. Pull the team project first so your branch workspace exists.",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
let dir = null;
|
|
502
|
+
let projectFiles = null;
|
|
503
|
+
if (Number(localProject?.metadata?.buildId || 0) === contributionBuildId) {
|
|
504
|
+
dir = resolveProjectDirForSave({ options, localProject });
|
|
505
|
+
projectFiles = await collectProjectFiles(dir);
|
|
506
|
+
}
|
|
507
|
+
const result = await requestJson({
|
|
508
|
+
method: "POST",
|
|
509
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/update-from-main`,
|
|
510
|
+
authToken: auth.token,
|
|
511
|
+
body: projectFiles ? { projectFiles } : {},
|
|
512
|
+
timeoutMs: options.timeoutMs,
|
|
513
|
+
});
|
|
514
|
+
const mergedFiles = Array.isArray(result.projectFiles)
|
|
515
|
+
? result.projectFiles
|
|
516
|
+
: [];
|
|
517
|
+
if (dir && mergedFiles.length > 0) {
|
|
518
|
+
await writeProjectFiles({ dir, files: mergedFiles });
|
|
519
|
+
await removeLocalProjectFilesNotIn({ dir, files: mergedFiles });
|
|
520
|
+
const refreshed = await loadBuildMetadata({
|
|
521
|
+
options,
|
|
522
|
+
auth,
|
|
523
|
+
buildId: contributionBuildId,
|
|
524
|
+
}).catch(() => null);
|
|
525
|
+
if (refreshed) {
|
|
526
|
+
await writeProjectMetadata({
|
|
527
|
+
dir,
|
|
528
|
+
options,
|
|
529
|
+
build: refreshed,
|
|
530
|
+
manifest: localProject?.metadata?.manifest || null,
|
|
531
|
+
pulledAt: new Date().toISOString(),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
printUpdateFromMainResult({ result, build, dir, mergedFiles });
|
|
536
|
+
}
|
|
537
|
+
|
|
427
538
|
async function reference(options) {
|
|
428
539
|
const auth = await resolveAuth(options);
|
|
429
540
|
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
@@ -1579,33 +1690,95 @@ async function pullReferenceFiles({ options, auth, buildId }) {
|
|
|
1579
1690
|
};
|
|
1580
1691
|
}
|
|
1581
1692
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1693
|
+
// Read-only checkout of a team project's MAIN workspace. Collaborators edit on
|
|
1694
|
+
// their contribution branch; this exists so branch work can consult what main
|
|
1695
|
+
// currently looks like without the pull auto-redirecting to the branch.
|
|
1696
|
+
async function pullMainBuildFiles({ options, auth, build }) {
|
|
1697
|
+
const rootBuildId =
|
|
1698
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
1699
|
+
if (!rootBuildId) {
|
|
1700
|
+
throw new Error("Could not resolve the team project for --main.");
|
|
1701
|
+
}
|
|
1702
|
+
const result = await loadBuildFiles({
|
|
1703
|
+
options,
|
|
1704
|
+
auth,
|
|
1705
|
+
buildId: rootBuildId,
|
|
1706
|
+
includeContent: true,
|
|
1707
|
+
});
|
|
1708
|
+
const rootBuild = result.build || { id: rootBuildId, title: `Build ${rootBuildId}` };
|
|
1709
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
1710
|
+
// Never nest a main checkout inside another Lumine workspace (a later save
|
|
1711
|
+
// there would upload it as project files) — default to a sibling instead.
|
|
1712
|
+
// Running from a main checkout of this same build refreshes it in place,
|
|
1713
|
+
// and the refresh prunes files main has deleted (a snapshot must not lie).
|
|
1714
|
+
const enclosing = options.dir
|
|
1715
|
+
? null
|
|
1716
|
+
: await findLocalProjectMetadata(process.cwd());
|
|
1717
|
+
const enclosingIsThisMain =
|
|
1718
|
+
enclosing?.metadata?.mainCheckout === true &&
|
|
1719
|
+
Number(enclosing.metadata.buildId || 0) === rootBuildId;
|
|
1720
|
+
const dir = path.resolve(
|
|
1721
|
+
options.dir ||
|
|
1722
|
+
(enclosingIsThisMain
|
|
1723
|
+
? enclosing.rootDir
|
|
1724
|
+
: enclosing
|
|
1725
|
+
? path.join(path.dirname(enclosing.rootDir), defaultMainCheckoutDir(rootBuild))
|
|
1726
|
+
: defaultMainCheckoutDir(rootBuild)),
|
|
1727
|
+
);
|
|
1728
|
+
await writeProjectFiles({ dir, files });
|
|
1729
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
1730
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
1594
1731
|
}
|
|
1732
|
+
await writeInstructionFiles({
|
|
1733
|
+
dir,
|
|
1734
|
+
marker: LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
1735
|
+
content: LUMINE_MAIN_CHECKOUT_INSTRUCTIONS,
|
|
1736
|
+
});
|
|
1737
|
+
await writeSdkReference({ dir });
|
|
1738
|
+
await writeMainCheckoutMetadata({
|
|
1739
|
+
dir,
|
|
1740
|
+
options,
|
|
1741
|
+
build: rootBuild,
|
|
1742
|
+
manifest: result.projectManifest || null,
|
|
1743
|
+
pulledAt: new Date().toISOString(),
|
|
1744
|
+
});
|
|
1745
|
+
console.log(`Pulled main for ${formatBuildTitle(rootBuild)} (read-only).`);
|
|
1746
|
+
console.log(`Pulled ${files.length} file${files.length === 1 ? "" : "s"} to ${dir}`);
|
|
1747
|
+
console.log(
|
|
1748
|
+
`Edits belong on your branch: \`lumine pull ${rootBuildId}\`, and \`lumine update-from-main\` brings main's changes into it.`,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
async function writeAgentInstructions({ dir }) {
|
|
1753
|
+
await writeInstructionFiles({
|
|
1754
|
+
dir,
|
|
1755
|
+
marker: LUMINE_AGENT_INSTRUCTIONS_MARKER,
|
|
1756
|
+
content: LUMINE_AGENT_INSTRUCTIONS,
|
|
1757
|
+
});
|
|
1595
1758
|
}
|
|
1596
1759
|
|
|
1597
1760
|
async function writeReferenceInstructions({ dir }) {
|
|
1761
|
+
await writeInstructionFiles({
|
|
1762
|
+
dir,
|
|
1763
|
+
marker: LUMINE_REFERENCE_INSTRUCTIONS_MARKER,
|
|
1764
|
+
content: LUMINE_REFERENCE_INSTRUCTIONS,
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Write AGENTS.md/CLAUDE.md, but never clobber a file the user customized:
|
|
1769
|
+
// only (re)write when the file is absent or still carries our marker.
|
|
1770
|
+
async function writeInstructionFiles({ dir, marker, content }) {
|
|
1598
1771
|
for (const fileName of AGENT_INSTRUCTION_FILES) {
|
|
1599
1772
|
const filePath = path.join(dir, fileName);
|
|
1600
1773
|
try {
|
|
1601
1774
|
const existing = await fs.readFile(filePath, "utf8");
|
|
1602
|
-
if (!existing.includes(
|
|
1775
|
+
if (!existing.includes(marker)) {
|
|
1603
1776
|
continue;
|
|
1604
1777
|
}
|
|
1605
1778
|
} catch (error) {
|
|
1606
1779
|
if (error.code !== "ENOENT") throw error;
|
|
1607
1780
|
}
|
|
1608
|
-
await fs.writeFile(filePath,
|
|
1781
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
1609
1782
|
}
|
|
1610
1783
|
}
|
|
1611
1784
|
|
|
@@ -1660,6 +1833,14 @@ async function collectProjectFilesFromDir({ root, dir, files }) {
|
|
|
1660
1833
|
if (entry.isFile() && EXCLUDED_UPLOAD_FILES.has(entry.name)) continue;
|
|
1661
1834
|
const fullPath = path.join(dir, entry.name);
|
|
1662
1835
|
if (entry.isDirectory()) {
|
|
1836
|
+
// A nested Lumine checkout (its own .twinkle metadata) is another
|
|
1837
|
+
// project that happens to sit here — never upload it as project files.
|
|
1838
|
+
if (await isNestedLumineCheckout(fullPath)) {
|
|
1839
|
+
console.error(
|
|
1840
|
+
`lumine: skipping nested Lumine checkout ${path.relative(root, fullPath)}/ (not part of this project)`,
|
|
1841
|
+
);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1663
1844
|
await collectProjectFilesFromDir({ root, dir: fullPath, files });
|
|
1664
1845
|
continue;
|
|
1665
1846
|
}
|
|
@@ -1681,6 +1862,15 @@ async function collectProjectFilesFromDir({ root, dir, files }) {
|
|
|
1681
1862
|
}
|
|
1682
1863
|
}
|
|
1683
1864
|
|
|
1865
|
+
async function isNestedLumineCheckout(dir) {
|
|
1866
|
+
try {
|
|
1867
|
+
await fs.access(path.join(dir, PROJECT_METADATA_DIR, PROJECT_METADATA_FILE));
|
|
1868
|
+
return true;
|
|
1869
|
+
} catch {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1684
1874
|
function localFilePathToProjectPath({ root, filePath }) {
|
|
1685
1875
|
const relative = path.relative(root, filePath).replace(/\\/g, "/");
|
|
1686
1876
|
if (!relative || relative.startsWith("../") || path.isAbsolute(relative)) {
|
|
@@ -1764,6 +1954,19 @@ async function writeProjectFiles({ dir, files }) {
|
|
|
1764
1954
|
}
|
|
1765
1955
|
}
|
|
1766
1956
|
|
|
1957
|
+
// After update-from-main, files that main deleted must also leave the local
|
|
1958
|
+
// workspace — otherwise the next save would resurrect them on the branch.
|
|
1959
|
+
async function removeLocalProjectFilesNotIn({ dir, files }) {
|
|
1960
|
+
const keep = new Set(files.map((file) => String(file.path)));
|
|
1961
|
+
const localFiles = await collectProjectFiles(dir);
|
|
1962
|
+
for (const file of localFiles) {
|
|
1963
|
+
if (keep.has(file.path)) continue;
|
|
1964
|
+
await fs.unlink(
|
|
1965
|
+
resolveLocalProjectFilePath({ rootDir: dir, projectPath: file.path }),
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1767
1970
|
async function writeProjectMetadata({
|
|
1768
1971
|
dir,
|
|
1769
1972
|
options,
|
|
@@ -1864,6 +2067,50 @@ async function writeReferenceMetadata({
|
|
|
1864
2067
|
);
|
|
1865
2068
|
}
|
|
1866
2069
|
|
|
2070
|
+
// Metadata for a read-only `pull --main` checkout: canWrite is forced false
|
|
2071
|
+
// (the files endpoint reports token scope, not role) and mainCheckout marks it
|
|
2072
|
+
// so save can explain where edits belong.
|
|
2073
|
+
async function writeMainCheckoutMetadata({
|
|
2074
|
+
dir,
|
|
2075
|
+
options,
|
|
2076
|
+
build,
|
|
2077
|
+
manifest,
|
|
2078
|
+
pulledAt,
|
|
2079
|
+
}) {
|
|
2080
|
+
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
2081
|
+
await fs.mkdir(metadataDir, { recursive: true });
|
|
2082
|
+
const rootBuildId = Number(build?.id || 0) || null;
|
|
2083
|
+
await fs.writeFile(
|
|
2084
|
+
path.join(metadataDir, PROJECT_METADATA_FILE),
|
|
2085
|
+
JSON.stringify(
|
|
2086
|
+
{
|
|
2087
|
+
schemaVersion: 1,
|
|
2088
|
+
buildId: rootBuildId,
|
|
2089
|
+
readOnly: true,
|
|
2090
|
+
mainCheckout: true,
|
|
2091
|
+
build: {
|
|
2092
|
+
id: rootBuildId,
|
|
2093
|
+
title: build?.title || (rootBuildId ? `Build ${rootBuildId}` : ""),
|
|
2094
|
+
role: build?.role || "collaborator",
|
|
2095
|
+
ownerUsername: build?.ownerUsername || null,
|
|
2096
|
+
contributionStatus: "none",
|
|
2097
|
+
contributionRootBuildId: null,
|
|
2098
|
+
canWrite: false,
|
|
2099
|
+
canPublish: false,
|
|
2100
|
+
},
|
|
2101
|
+
apiUrl: options.apiUrl,
|
|
2102
|
+
siteUrl: options.siteUrl,
|
|
2103
|
+
lumineCli: serializeLumineCliMetadata(options),
|
|
2104
|
+
manifest,
|
|
2105
|
+
pulledAt,
|
|
2106
|
+
},
|
|
2107
|
+
null,
|
|
2108
|
+
2,
|
|
2109
|
+
),
|
|
2110
|
+
"utf8",
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
1867
2114
|
async function findLocalProjectMetadata(startDir) {
|
|
1868
2115
|
let current = path.resolve(startDir || process.cwd());
|
|
1869
2116
|
while (true) {
|
|
@@ -1893,6 +2140,13 @@ function resolveProjectDirForSave({ options, localProject }) {
|
|
|
1893
2140
|
function assertLocalProjectCanBeSaved(localProject) {
|
|
1894
2141
|
const metadata = localProject?.metadata;
|
|
1895
2142
|
if (!metadata) return;
|
|
2143
|
+
if (metadata.mainCheckout === true) {
|
|
2144
|
+
const rootBuildId =
|
|
2145
|
+
Number(metadata.buildId || 0) || Number(metadata.build?.id || 0) || 0;
|
|
2146
|
+
throw new Error(
|
|
2147
|
+
`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.`,
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
1896
2150
|
if (isReadOnlyReferenceMetadata(metadata)) {
|
|
1897
2151
|
const sourceBuildId =
|
|
1898
2152
|
Number(metadata.reference?.sourceBuildId || 0) ||
|
|
@@ -2159,6 +2413,40 @@ function printContributionDiff({ result, build }) {
|
|
|
2159
2413
|
}
|
|
2160
2414
|
}
|
|
2161
2415
|
|
|
2416
|
+
function printUpdateFromMainResult({ result, build, dir, mergedFiles }) {
|
|
2417
|
+
const branchNumber = Number(build?.contributionBranchNumber || 0) || 0;
|
|
2418
|
+
const branchLabel = branchNumber
|
|
2419
|
+
? `branch ${branchNumber}`
|
|
2420
|
+
: `branch #${build?.id}`;
|
|
2421
|
+
const autoMerged = Array.isArray(result.autoMergedPaths)
|
|
2422
|
+
? result.autoMergedPaths
|
|
2423
|
+
: [];
|
|
2424
|
+
const conflicts = Array.isArray(result.conflicts) ? result.conflicts : [];
|
|
2425
|
+
console.log(
|
|
2426
|
+
`Updated ${branchLabel} from main (${mergedFiles.length} file${mergedFiles.length === 1 ? "" : "s"}).`,
|
|
2427
|
+
);
|
|
2428
|
+
if (autoMerged.length > 0) {
|
|
2429
|
+
console.log(`Auto-merged: ${autoMerged.join(", ")}`);
|
|
2430
|
+
}
|
|
2431
|
+
if (conflicts.length > 0) {
|
|
2432
|
+
const conflictPaths = conflicts.map((conflict) =>
|
|
2433
|
+
typeof conflict === "string" ? conflict : conflict?.path || "unknown",
|
|
2434
|
+
);
|
|
2435
|
+
console.log(`CONFLICTS (markers written): ${conflictPaths.join(", ")}`);
|
|
2436
|
+
console.log(
|
|
2437
|
+
dir
|
|
2438
|
+
? "Resolve the <<<<<<< / >>>>>>> markers in the files above, then run `lumine save`."
|
|
2439
|
+
: "Pull the branch, resolve the <<<<<<< / >>>>>>> markers, then run `lumine save`.",
|
|
2440
|
+
);
|
|
2441
|
+
} else if (!dir) {
|
|
2442
|
+
console.log(
|
|
2443
|
+
"The branch was updated on Twinkle. Run `lumine pull` to refresh your local workspace.",
|
|
2444
|
+
);
|
|
2445
|
+
} else {
|
|
2446
|
+
console.log(`Local workspace updated: ${dir}`);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2162
2450
|
function printContributionActionResult({
|
|
2163
2451
|
action,
|
|
2164
2452
|
result,
|
|
@@ -2430,6 +2718,7 @@ function parseArgs(args) {
|
|
|
2430
2718
|
"save",
|
|
2431
2719
|
"noUpdateCheck",
|
|
2432
2720
|
"allowWrite",
|
|
2721
|
+
"main",
|
|
2433
2722
|
]);
|
|
2434
2723
|
|
|
2435
2724
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2508,6 +2797,7 @@ function parseArgs(args) {
|
|
|
2508
2797
|
null,
|
|
2509
2798
|
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
2510
2799
|
dir: raw.dir ? String(raw.dir) : "",
|
|
2800
|
+
pullMain: parseBoolean(raw.main, false),
|
|
2511
2801
|
summary: raw.summary ? String(raw.summary) : "",
|
|
2512
2802
|
publish: parseBoolean(raw.publish, false),
|
|
2513
2803
|
saveFirst: parseBoolean(raw.save, false),
|
|
@@ -2550,6 +2840,22 @@ async function resolveRequiredBuildIdOrSelected(
|
|
|
2550
2840
|
(await findLocalProjectMetadata(
|
|
2551
2841
|
path.resolve(options.dir || process.cwd()),
|
|
2552
2842
|
));
|
|
2843
|
+
// A `pull --main` checkout is read-only but still knows its root build id.
|
|
2844
|
+
// Read-only commands run from it (check, diff, a refreshing `pull --main`)
|
|
2845
|
+
// resolve that id instead of bouncing off the reference/fork error — but
|
|
2846
|
+
// mutating commands (launch would PUBLISH main, save, merge, …) stay blocked
|
|
2847
|
+
// with main-checkout-specific guidance.
|
|
2848
|
+
if (resolvedLocalProject?.metadata?.mainCheckout === true) {
|
|
2849
|
+
const mainBuildId =
|
|
2850
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
2851
|
+
Number(resolvedLocalProject.metadata.build?.id || 0);
|
|
2852
|
+
if (!MAIN_CHECKOUT_READONLY_COMMANDS.has(options.command)) {
|
|
2853
|
+
throw new Error(
|
|
2854
|
+
`This is a read-only checkout of main${mainBuildId ? ` for Build ${mainBuildId}` : ""}; \`lumine ${options.command}\` isn't available here. Run it from your branch workspace or the canonical checkout, or pass an explicit Build URL.`,
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
if (mainBuildId > 0) return mainBuildId;
|
|
2858
|
+
}
|
|
2553
2859
|
if (
|
|
2554
2860
|
resolvedLocalProject?.metadata &&
|
|
2555
2861
|
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
@@ -2768,6 +3074,12 @@ function defaultReferenceDir(build) {
|
|
|
2768
3074
|
return `twinkle-reference-${titleSlug || "build"}-${buildId}`;
|
|
2769
3075
|
}
|
|
2770
3076
|
|
|
3077
|
+
function defaultMainCheckoutDir(build) {
|
|
3078
|
+
const titleSlug = slugify(build?.title || "");
|
|
3079
|
+
const buildId = Number(build?.id || 0) || "build";
|
|
3080
|
+
return `twinkle-main-${titleSlug || "build"}-${buildId}`;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
2771
3083
|
function slugify(value) {
|
|
2772
3084
|
return String(value || "")
|
|
2773
3085
|
.toLowerCase()
|
|
@@ -2791,11 +3103,13 @@ function printHelp() {
|
|
|
2791
3103
|
lumine explore [search terms]
|
|
2792
3104
|
lumine select [twinkle-build-url]
|
|
2793
3105
|
lumine pull [twinkle-build-url]
|
|
3106
|
+
lumine pull [twinkle-build-url] --main
|
|
2794
3107
|
lumine reference <twinkle-build-url>
|
|
2795
3108
|
lumine fork <twinkle-build-url>
|
|
2796
3109
|
lumine diff <twinkle-branch-url>
|
|
2797
3110
|
lumine merge <twinkle-branch-url>
|
|
2798
3111
|
lumine replace-main <twinkle-branch-url>
|
|
3112
|
+
lumine update-from-main [twinkle-branch-url]
|
|
2799
3113
|
lumine save
|
|
2800
3114
|
lumine check [twinkle-build-url]
|
|
2801
3115
|
lumine launch [twinkle-build-url]
|
|
@@ -2812,6 +3126,8 @@ Examples:
|
|
|
2812
3126
|
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
2813
3127
|
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
2814
3128
|
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
3129
|
+
npx @stage5/lumine@latest pull 884 --main
|
|
3130
|
+
npx @stage5/lumine@latest update-from-main
|
|
2815
3131
|
npx @stage5/lumine@latest pull
|
|
2816
3132
|
npx @stage5/lumine@latest save
|
|
2817
3133
|
npx @stage5/lumine@latest save --publish
|
|
@@ -2827,6 +3143,7 @@ Options:
|
|
|
2827
3143
|
--auth-file <path> Saved login path
|
|
2828
3144
|
--auth-token <token> Override saved login
|
|
2829
3145
|
--dir <path> Directory for pulled project files
|
|
3146
|
+
--main With pull: read-only checkout of the team project's main
|
|
2830
3147
|
--title <text> New Build title
|
|
2831
3148
|
--description <text> Optional New Build description
|
|
2832
3149
|
--no-description Skip the New Build description prompt
|