@stage5/lumine 0.2.3 → 0.2.5
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 +1 -2841
- package/lib/api.js +381 -0
- package/lib/assets.js +670 -0
- package/lib/auth.js +217 -0
- package/lib/commands.js +1491 -0
- package/lib/constants.js +256 -0
- package/lib/http.js +72 -0
- package/lib/sdk.js +489 -0
- package/lib/util.js +154 -0
- package/lib/workspace.js +577 -0
- package/package.json +5 -1
package/lib/commands.js
ADDED
|
@@ -0,0 +1,1491 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import readline from "readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "process";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ASSETS_METADATA_FILE,
|
|
7
|
+
COMMANDS,
|
|
8
|
+
DEFAULT_API_URL,
|
|
9
|
+
DEFAULT_AUTH_FILE,
|
|
10
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
11
|
+
DEFAULT_PROJECT_LIMIT,
|
|
12
|
+
DEFAULT_SAVE_SUMMARY,
|
|
13
|
+
DEFAULT_SITE_URL,
|
|
14
|
+
DEFAULT_TIMEOUT_MS,
|
|
15
|
+
LUMINE_MAIN_CHECKOUT_INSTRUCTIONS,
|
|
16
|
+
LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
17
|
+
MAIN_CHECKOUT_READONLY_COMMANDS,
|
|
18
|
+
PROJECT_METADATA_DIR,
|
|
19
|
+
SDK_REFERENCE_FILE,
|
|
20
|
+
} from "./constants.js";
|
|
21
|
+
import {
|
|
22
|
+
createBuild,
|
|
23
|
+
fetchAllRuntimeAssets,
|
|
24
|
+
forkBuild,
|
|
25
|
+
listBuilds,
|
|
26
|
+
listOpenSourceBuilds,
|
|
27
|
+
loadBuildFiles,
|
|
28
|
+
loadBuildMetadata,
|
|
29
|
+
loadContributionDiff,
|
|
30
|
+
loadLumineCliVersionInfo,
|
|
31
|
+
loadOpenSourceBuildFiles,
|
|
32
|
+
maybeCheckForLumineCliUpdate,
|
|
33
|
+
mergeContributionIntoMain,
|
|
34
|
+
mintBuildApiToken,
|
|
35
|
+
publishBuild,
|
|
36
|
+
replaceMainWithContribution,
|
|
37
|
+
resolveBranchBuild,
|
|
38
|
+
saveProjectFiles,
|
|
39
|
+
} from "./api.js";
|
|
40
|
+
import { assetsCommand, writeAssetsManifest } from "./assets.js";
|
|
41
|
+
import {
|
|
42
|
+
assertAuthScope,
|
|
43
|
+
ensureAuth,
|
|
44
|
+
login,
|
|
45
|
+
logout,
|
|
46
|
+
resolveAuth,
|
|
47
|
+
saveSelectedBuild,
|
|
48
|
+
whoami,
|
|
49
|
+
} from "./auth.js";
|
|
50
|
+
import { probeUrl, requestJson } from "./http.js";
|
|
51
|
+
import { sdkCommand } from "./sdk.js";
|
|
52
|
+
import {
|
|
53
|
+
defaultMainCheckoutDir,
|
|
54
|
+
defaultReferenceDir,
|
|
55
|
+
defaultWorkspaceDir,
|
|
56
|
+
parseBoolean,
|
|
57
|
+
resolveBuildId,
|
|
58
|
+
resolveBuildReference,
|
|
59
|
+
shellQuote,
|
|
60
|
+
toCamelCase,
|
|
61
|
+
trimTrailingSlash,
|
|
62
|
+
} from "./util.js";
|
|
63
|
+
import {
|
|
64
|
+
assertLocalProjectCanBeSaved,
|
|
65
|
+
assertProjectFilesWithinLimits,
|
|
66
|
+
collectProjectFiles,
|
|
67
|
+
collectProjectLimitFindings,
|
|
68
|
+
findLocalProjectMetadata,
|
|
69
|
+
isIndexHtmlPath,
|
|
70
|
+
isReadOnlyReferenceMetadata,
|
|
71
|
+
removeLocalProjectFilesNotIn,
|
|
72
|
+
resolveProjectDirForSave,
|
|
73
|
+
writeAgentInstructions,
|
|
74
|
+
writeInstructionFiles,
|
|
75
|
+
writeMainCheckoutMetadata,
|
|
76
|
+
writeProjectFiles,
|
|
77
|
+
writeProjectMetadata,
|
|
78
|
+
writeReferenceInstructions,
|
|
79
|
+
writeReferenceMetadata,
|
|
80
|
+
writeSdkReference,
|
|
81
|
+
} from "./workspace.js";
|
|
82
|
+
|
|
83
|
+
export async function main() {
|
|
84
|
+
const options = parseArgs(process.argv.slice(2));
|
|
85
|
+
options.lumineCli = await loadLumineCliVersionInfo({ options });
|
|
86
|
+
if (options.help) {
|
|
87
|
+
printHelp();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (options.updateCheck) {
|
|
91
|
+
await maybeCheckForLumineCliUpdate({ options });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.command === "workspace") {
|
|
95
|
+
await workspace(options);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (options.command === "login") {
|
|
99
|
+
await login(options);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (options.command === "logout") {
|
|
103
|
+
await logout(options);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (options.command === "whoami") {
|
|
107
|
+
await whoami(options);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (options.command === "new") {
|
|
111
|
+
await newBuild(options);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (options.command === "projects") {
|
|
115
|
+
await projects(options);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (options.command === "explore") {
|
|
119
|
+
await explore(options);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (options.command === "select") {
|
|
123
|
+
await selectProject(options);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (options.command === "pull") {
|
|
127
|
+
await pull(options);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (options.command === "reference") {
|
|
131
|
+
await reference(options);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (options.command === "fork") {
|
|
135
|
+
await fork(options);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (options.command === "diff") {
|
|
139
|
+
await diff(options);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (options.command === "merge") {
|
|
143
|
+
await mergeBranch(options);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (options.command === "replace-main") {
|
|
147
|
+
await replaceMainWithBranch(options);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (options.command === "update-from-main") {
|
|
151
|
+
await updateBranchFromMain(options);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (options.command === "save" || options.command === "push") {
|
|
155
|
+
await save(options);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (options.command === "check") {
|
|
159
|
+
await check(options);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (options.command === "launch") {
|
|
163
|
+
await launch(options);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (options.command === "sdk") {
|
|
167
|
+
await sdkCommand(options);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (options.command === "assets") {
|
|
171
|
+
await assetsCommand(options);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
printHelp();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function newBuild(options) {
|
|
179
|
+
const title = await resolveNewBuildTitle(options);
|
|
180
|
+
const description = await resolveNewBuildDescription(options);
|
|
181
|
+
const auth = await ensureAuth(options);
|
|
182
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
183
|
+
const createResult = await createBuild({
|
|
184
|
+
options,
|
|
185
|
+
auth,
|
|
186
|
+
title,
|
|
187
|
+
description,
|
|
188
|
+
});
|
|
189
|
+
const buildId = Number(createResult.build?.id || 0);
|
|
190
|
+
if (!buildId) {
|
|
191
|
+
throw new Error("Twinkle did not return a created Build.");
|
|
192
|
+
}
|
|
193
|
+
const result = await pullBuildFiles({ options, auth, buildId });
|
|
194
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
195
|
+
printNewBuildResult({ createResult, pullResult: result });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function workspace(options) {
|
|
199
|
+
const auth = await ensureAuth(options);
|
|
200
|
+
const selectedBuild = options.target
|
|
201
|
+
? await loadTargetBuildMetadata({ options, auth })
|
|
202
|
+
: await chooseProject({
|
|
203
|
+
builds: await listBuilds({ options, auth }),
|
|
204
|
+
});
|
|
205
|
+
const build = await resolveEditableWorkspaceBuild({
|
|
206
|
+
options,
|
|
207
|
+
auth,
|
|
208
|
+
build: selectedBuild,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await saveSelectedBuild({ options, auth, build });
|
|
212
|
+
const result = await pullBuildFiles({ options, auth, buildId: build.id });
|
|
213
|
+
await saveSelectedBuild({ options, auth, build: result.build || build });
|
|
214
|
+
printPullResult(result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function projects(options) {
|
|
218
|
+
const auth = await resolveAuth(options);
|
|
219
|
+
const builds = await listBuilds({ options, auth });
|
|
220
|
+
printBuildList(builds);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function explore(options) {
|
|
224
|
+
const auth = await resolveAuth(options);
|
|
225
|
+
const builds = await listOpenSourceBuilds({ options, auth });
|
|
226
|
+
printOpenSourceBuildList(builds, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function selectProject(options) {
|
|
230
|
+
const auth = await resolveAuth(options);
|
|
231
|
+
const selectedBuild = options.target
|
|
232
|
+
? await loadTargetBuildMetadata({ options, auth })
|
|
233
|
+
: await chooseProject({
|
|
234
|
+
builds: await listBuilds({ options, auth }),
|
|
235
|
+
});
|
|
236
|
+
const build = await resolveEditableWorkspaceBuild({
|
|
237
|
+
options,
|
|
238
|
+
auth,
|
|
239
|
+
build: selectedBuild,
|
|
240
|
+
});
|
|
241
|
+
await saveSelectedBuild({ options, auth, build });
|
|
242
|
+
console.log(
|
|
243
|
+
`Selected ${formatBuildTitle(build)}. Run \`lumine pull\` to get the files.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function pull(options) {
|
|
248
|
+
const auth = await resolveAuth(options);
|
|
249
|
+
const localProject = await findLocalProjectMetadata(
|
|
250
|
+
path.resolve(options.dir || process.cwd()),
|
|
251
|
+
);
|
|
252
|
+
const requestedBuildId = await resolveRequiredBuildIdOrSelected(
|
|
253
|
+
options,
|
|
254
|
+
auth,
|
|
255
|
+
options.pullMain ? { localProject } : {},
|
|
256
|
+
);
|
|
257
|
+
const selectedBuild = await loadBuildMetadata({
|
|
258
|
+
options,
|
|
259
|
+
auth,
|
|
260
|
+
buildId: requestedBuildId,
|
|
261
|
+
});
|
|
262
|
+
if (options.pullMain) {
|
|
263
|
+
await pullMainBuildFiles({ options, auth, build: selectedBuild });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const build = await resolveEditableWorkspaceBuild({
|
|
267
|
+
options,
|
|
268
|
+
auth,
|
|
269
|
+
build: selectedBuild,
|
|
270
|
+
});
|
|
271
|
+
const result = await pullBuildFiles({ options, auth, buildId: build.id });
|
|
272
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
273
|
+
printPullResult(result);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Sync a contribution branch with its team project's main: the server runs a
|
|
277
|
+
// three-way merge (auto-merging where it can, writing git-style conflict
|
|
278
|
+
// markers where it cannot) and saves the result to the branch. When run inside
|
|
279
|
+
// the branch's workspace, local edits are sent along as the branch's pending
|
|
280
|
+
// state and the merged files are written back to disk.
|
|
281
|
+
export async function updateBranchFromMain(options) {
|
|
282
|
+
const auth = await resolveAuth(options);
|
|
283
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
284
|
+
const localProject = await findLocalProjectMetadata(
|
|
285
|
+
path.resolve(options.dir || process.cwd()),
|
|
286
|
+
);
|
|
287
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
288
|
+
localProject,
|
|
289
|
+
});
|
|
290
|
+
const build = await loadBuildMetadata({ options, auth, buildId });
|
|
291
|
+
const rootBuildId = Number(build?.contributionRootBuildId || 0);
|
|
292
|
+
const contributionBuildId = Number(build?.id || 0);
|
|
293
|
+
if (!rootBuildId || !contributionBuildId) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
"update-from-main only applies to contribution branches of a team project. Pull the team project first so your branch workspace exists.",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
let dir = null;
|
|
299
|
+
let projectFiles = null;
|
|
300
|
+
if (Number(localProject?.metadata?.buildId || 0) === contributionBuildId) {
|
|
301
|
+
dir = resolveProjectDirForSave({ options, localProject });
|
|
302
|
+
projectFiles = await collectProjectFiles(dir);
|
|
303
|
+
}
|
|
304
|
+
const result = await requestJson({
|
|
305
|
+
method: "POST",
|
|
306
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/${contributionBuildId}/update-from-main`,
|
|
307
|
+
authToken: auth.token,
|
|
308
|
+
body: projectFiles ? { projectFiles } : {},
|
|
309
|
+
timeoutMs: options.timeoutMs,
|
|
310
|
+
});
|
|
311
|
+
const mergedFiles = Array.isArray(result.projectFiles)
|
|
312
|
+
? result.projectFiles
|
|
313
|
+
: [];
|
|
314
|
+
if (dir && mergedFiles.length > 0) {
|
|
315
|
+
await writeProjectFiles({ dir, files: mergedFiles });
|
|
316
|
+
await removeLocalProjectFilesNotIn({ dir, files: mergedFiles });
|
|
317
|
+
const refreshed = await loadBuildMetadata({
|
|
318
|
+
options,
|
|
319
|
+
auth,
|
|
320
|
+
buildId: contributionBuildId,
|
|
321
|
+
}).catch(() => null);
|
|
322
|
+
if (refreshed) {
|
|
323
|
+
await writeProjectMetadata({
|
|
324
|
+
dir,
|
|
325
|
+
options,
|
|
326
|
+
build: refreshed,
|
|
327
|
+
manifest: localProject?.metadata?.manifest || null,
|
|
328
|
+
pulledAt: new Date().toISOString(),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
printUpdateFromMainResult({ result, build, dir, mergedFiles });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function reference(options) {
|
|
336
|
+
const auth = await resolveAuth(options);
|
|
337
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
338
|
+
const result = await pullReferenceFiles({ options, auth, buildId });
|
|
339
|
+
printReferenceResult(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function fork(options) {
|
|
343
|
+
const auth = await resolveAuth(options);
|
|
344
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
345
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
346
|
+
const forkResult = await forkBuild({ options, auth, buildId });
|
|
347
|
+
const forkedBuildId = Number(forkResult.build?.id || 0);
|
|
348
|
+
if (!forkedBuildId) {
|
|
349
|
+
throw new Error("Twinkle did not return a forked Build.");
|
|
350
|
+
}
|
|
351
|
+
const result = await pullBuildFiles({
|
|
352
|
+
options,
|
|
353
|
+
auth,
|
|
354
|
+
buildId: forkedBuildId,
|
|
355
|
+
});
|
|
356
|
+
await saveSelectedBuild({ options, auth, build: result.build });
|
|
357
|
+
printForkResult({ forkResult, pullResult: result });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function diff(options) {
|
|
361
|
+
const auth = await resolveAuth(options);
|
|
362
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
363
|
+
const { rootBuildId, contributionBuildId } =
|
|
364
|
+
resolveContributionActionBuildIds(build);
|
|
365
|
+
const result = await loadContributionDiff({
|
|
366
|
+
options,
|
|
367
|
+
auth,
|
|
368
|
+
rootBuildId,
|
|
369
|
+
contributionBuildId,
|
|
370
|
+
});
|
|
371
|
+
printContributionDiff({ result, build });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function mergeBranch(options) {
|
|
375
|
+
const auth = await resolveAuth(options);
|
|
376
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
377
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
378
|
+
const { rootBuildId, contributionBuildId } =
|
|
379
|
+
resolveContributionActionBuildIds(build);
|
|
380
|
+
const result = await mergeContributionIntoMain({
|
|
381
|
+
options,
|
|
382
|
+
auth,
|
|
383
|
+
rootBuildId,
|
|
384
|
+
contributionBuildId,
|
|
385
|
+
});
|
|
386
|
+
printContributionActionResult({
|
|
387
|
+
action: "Merged",
|
|
388
|
+
result,
|
|
389
|
+
rootBuildId,
|
|
390
|
+
contributionBuildId,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function replaceMainWithBranch(options) {
|
|
395
|
+
const auth = await resolveAuth(options);
|
|
396
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
397
|
+
const build = await loadTargetBuildMetadata({ options, auth });
|
|
398
|
+
const { rootBuildId, contributionBuildId } =
|
|
399
|
+
resolveContributionActionBuildIds(build);
|
|
400
|
+
const result = await replaceMainWithContribution({
|
|
401
|
+
options,
|
|
402
|
+
auth,
|
|
403
|
+
rootBuildId,
|
|
404
|
+
contributionBuildId,
|
|
405
|
+
});
|
|
406
|
+
printContributionActionResult({
|
|
407
|
+
action: "Replaced main with",
|
|
408
|
+
result,
|
|
409
|
+
rootBuildId,
|
|
410
|
+
contributionBuildId,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function save(options) {
|
|
415
|
+
const auth = await resolveAuth(options);
|
|
416
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
417
|
+
const localProject = await findLocalProjectMetadata(
|
|
418
|
+
path.resolve(options.dir || process.cwd()),
|
|
419
|
+
);
|
|
420
|
+
assertLocalProjectCanBeSaved(localProject);
|
|
421
|
+
let buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
|
|
422
|
+
localProject,
|
|
423
|
+
});
|
|
424
|
+
let build = await resolveBuildForSave({
|
|
425
|
+
options,
|
|
426
|
+
auth,
|
|
427
|
+
buildId,
|
|
428
|
+
localProject,
|
|
429
|
+
});
|
|
430
|
+
if (build?.canWrite === false) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
"This checkout is read-only for the current CLI login. Pull or diff it for review, then merge or replace main from the project owner workflow.",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
buildId = Number(build?.id || buildId);
|
|
436
|
+
const dir = resolveProjectDirForSave({ options, localProject });
|
|
437
|
+
const files = await collectProjectFiles(dir);
|
|
438
|
+
assertProjectFilesWithinLimits(files);
|
|
439
|
+
const result = await saveProjectFiles({
|
|
440
|
+
options,
|
|
441
|
+
auth,
|
|
442
|
+
buildId,
|
|
443
|
+
files,
|
|
444
|
+
summary: options.summary || DEFAULT_SAVE_SUMMARY,
|
|
445
|
+
});
|
|
446
|
+
build = result.build ||
|
|
447
|
+
build ||
|
|
448
|
+
(await loadBuildMetadata({ options, auth, buildId }).catch(() => null)) || {
|
|
449
|
+
id: buildId,
|
|
450
|
+
title: `Build ${buildId}`,
|
|
451
|
+
};
|
|
452
|
+
await saveSelectedBuild({ options, auth, build });
|
|
453
|
+
await writeProjectMetadata({
|
|
454
|
+
dir,
|
|
455
|
+
options,
|
|
456
|
+
build,
|
|
457
|
+
manifest: result.projectManifest || null,
|
|
458
|
+
lastSavedAt: new Date().toISOString(),
|
|
459
|
+
});
|
|
460
|
+
printSaveResult({ result, build, dir, files });
|
|
461
|
+
|
|
462
|
+
if (options.publish) {
|
|
463
|
+
if (build?.canPublish === false) {
|
|
464
|
+
console.log(
|
|
465
|
+
"Saved to your branch. The project owner can merge or replace main from Twinkle.",
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const publish = await publishBuild({ options, buildId, auth });
|
|
470
|
+
if (publish.skipped) {
|
|
471
|
+
console.log("Publish skipped: already up to date.");
|
|
472
|
+
} else {
|
|
473
|
+
console.log("Publish complete.");
|
|
474
|
+
}
|
|
475
|
+
console.log(`App: ${options.siteUrl}/app/${buildId}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function check(options) {
|
|
480
|
+
await reportLocalProjectFindings(options);
|
|
481
|
+
const auth = await resolveAuth(options);
|
|
482
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
483
|
+
const result = await requestJson({
|
|
484
|
+
url: `${options.apiUrl}/cli/build/${buildId}/launch-check`,
|
|
485
|
+
authToken: auth.token,
|
|
486
|
+
timeoutMs: options.timeoutMs,
|
|
487
|
+
});
|
|
488
|
+
printCheck(result);
|
|
489
|
+
if (!result.ok) process.exitCode = 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Local, network-free half of `lumine check`: validate the workspace against
|
|
493
|
+
// the platform's project-file rules so agents see violations before saving.
|
|
494
|
+
export async function reportLocalProjectFindings(options) {
|
|
495
|
+
const localProject = await findLocalProjectMetadata(
|
|
496
|
+
path.resolve(options.dir || process.cwd()),
|
|
497
|
+
);
|
|
498
|
+
if (!localProject?.rootDir || localProject?.metadata?.readOnly) return;
|
|
499
|
+
let files;
|
|
500
|
+
try {
|
|
501
|
+
files = await collectProjectFiles(localProject.rootDir);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error(`Local check failed: ${error?.message || error}`);
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const { errors, warnings } = collectProjectLimitFindings(files);
|
|
508
|
+
for (const warning of warnings) {
|
|
509
|
+
console.error(`Local check warning: ${warning}`);
|
|
510
|
+
}
|
|
511
|
+
for (const errorMessage of errors) {
|
|
512
|
+
console.error(`Local check error: ${errorMessage}`);
|
|
513
|
+
}
|
|
514
|
+
if (errors.length > 0) {
|
|
515
|
+
process.exitCode = 1;
|
|
516
|
+
} else {
|
|
517
|
+
console.log(
|
|
518
|
+
`Local check: ${files.length} project file${files.length === 1 ? "" : "s"} within limits.`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function launch(options) {
|
|
524
|
+
const auth = await resolveAuth(options);
|
|
525
|
+
if (options.saveFirst) {
|
|
526
|
+
await save({
|
|
527
|
+
...options,
|
|
528
|
+
publish: false,
|
|
529
|
+
saveFirst: false,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
533
|
+
const checkResult = await requestJson({
|
|
534
|
+
url: `${options.apiUrl}/cli/build/${buildId}/launch-check`,
|
|
535
|
+
authToken: auth.token,
|
|
536
|
+
timeoutMs: options.timeoutMs,
|
|
537
|
+
});
|
|
538
|
+
printCheck(checkResult);
|
|
539
|
+
const launchOk =
|
|
540
|
+
typeof checkResult.launchOk === "boolean"
|
|
541
|
+
? checkResult.launchOk
|
|
542
|
+
: checkResult.ok;
|
|
543
|
+
if (!checkResult.ok || !launchOk) {
|
|
544
|
+
process.exitCode = 1;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const publish = await publishBuild({ options, buildId, auth });
|
|
549
|
+
const build = publish.build || checkResult.build || {};
|
|
550
|
+
const appUrl = `${options.siteUrl}/app/${buildId}`;
|
|
551
|
+
const versionId =
|
|
552
|
+
Number(build.publishedArtifactVersionId || 0) ||
|
|
553
|
+
Number(build.releaseStatus?.publishedArtifactVersionId || 0) ||
|
|
554
|
+
0;
|
|
555
|
+
|
|
556
|
+
const appProbe = await probeUrl({
|
|
557
|
+
url: appUrl,
|
|
558
|
+
timeoutMs: options.timeoutMs,
|
|
559
|
+
});
|
|
560
|
+
const previewProbe = versionId
|
|
561
|
+
? await probeUrl({
|
|
562
|
+
url: `${options.apiUrl}/build/preview/build/${buildId}/version/${versionId}`,
|
|
563
|
+
authToken: auth.token,
|
|
564
|
+
timeoutMs: options.timeoutMs,
|
|
565
|
+
})
|
|
566
|
+
: null;
|
|
567
|
+
|
|
568
|
+
if (publish.skipped) {
|
|
569
|
+
console.log("Publish skipped: already up to date.");
|
|
570
|
+
} else {
|
|
571
|
+
console.log("Publish complete.");
|
|
572
|
+
}
|
|
573
|
+
console.log(`App: ${appUrl}`);
|
|
574
|
+
console.log(
|
|
575
|
+
`Prod shell: ${appProbe.ok ? "ok" : "fail"} ${appProbe.status} bytes=${appProbe.bytes}`,
|
|
576
|
+
);
|
|
577
|
+
if (previewProbe) {
|
|
578
|
+
console.log(
|
|
579
|
+
`Published preview: ${previewProbe.ok ? "ok" : "fail"} ` +
|
|
580
|
+
`${previewProbe.status} bytes=${previewProbe.bytes}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (!appProbe.ok || (previewProbe && !previewProbe.ok) || !versionId) {
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function printCheck(result) {
|
|
590
|
+
const checks = result.checks || {};
|
|
591
|
+
console.log(`Launch check: ${result.ok ? "ok" : "fail"}`);
|
|
592
|
+
if (checks.canonicalBuild) {
|
|
593
|
+
console.log(
|
|
594
|
+
`- canonical build: ${checks.canonicalBuild.ok ? "ok" : "fail"}`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
console.log(
|
|
598
|
+
`- project files: ${checks.projectFiles?.ok ? "ok" : "fail"} ` +
|
|
599
|
+
`files=${checks.projectFiles?.fileCount ?? 0}`,
|
|
600
|
+
);
|
|
601
|
+
console.log(`- toolchain: ${checks.toolchain?.ok ? "ok" : "fail"}`);
|
|
602
|
+
if (checks.publishPermission) {
|
|
603
|
+
console.log(
|
|
604
|
+
`- publish permission: ${checks.publishPermission.ok ? "ok" : "fail"}`,
|
|
605
|
+
);
|
|
606
|
+
if (checks.publishPermission.reason) {
|
|
607
|
+
console.log(` ${checks.publishPermission.reason}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (typeof result.launchOk === "boolean") {
|
|
611
|
+
console.log(`- launch gate: ${result.launchOk ? "ok" : "fail"}`);
|
|
612
|
+
}
|
|
613
|
+
console.log(
|
|
614
|
+
`- conflict markers: ${checks.conflictMarkers?.ok ? "ok" : "fail"}`,
|
|
615
|
+
);
|
|
616
|
+
for (const diagnostic of checks.toolchain?.diagnostics || []) {
|
|
617
|
+
console.log(
|
|
618
|
+
` ${diagnostic.kind} ${diagnostic.filePath}` +
|
|
619
|
+
`${diagnostic.line ? `:${diagnostic.line}` : ""} ${diagnostic.message}`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
for (const conflictPath of checks.conflictMarkers?.paths || []) {
|
|
623
|
+
console.log(` conflict marker: ${conflictPath}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function loadTargetBuildMetadata({ options, auth }) {
|
|
628
|
+
const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
|
|
629
|
+
return await loadBuildMetadata({ options, auth, buildId });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export async function resolveBuildForSave({ options, auth, buildId, localProject }) {
|
|
633
|
+
const localBuild = localProject?.metadata?.build;
|
|
634
|
+
const localBuildId =
|
|
635
|
+
Number(localBuild?.id || 0) ||
|
|
636
|
+
Number(localProject?.metadata?.buildId || 0);
|
|
637
|
+
const build =
|
|
638
|
+
localBuild && localBuildId === Number(buildId)
|
|
639
|
+
? { ...localBuild, id: buildId }
|
|
640
|
+
: await loadBuildMetadata({ options, auth, buildId });
|
|
641
|
+
return await resolveEditableWorkspaceBuild({
|
|
642
|
+
options,
|
|
643
|
+
auth,
|
|
644
|
+
build,
|
|
645
|
+
skipWriteScopeCheck: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export async function resolveEditableWorkspaceBuild({
|
|
650
|
+
options,
|
|
651
|
+
auth,
|
|
652
|
+
build,
|
|
653
|
+
skipWriteScopeCheck = false,
|
|
654
|
+
}) {
|
|
655
|
+
if (!shouldUseContributionBranch(build)) return build;
|
|
656
|
+
|
|
657
|
+
if (!skipWriteScopeCheck) {
|
|
658
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
659
|
+
}
|
|
660
|
+
const branch = await ensureDefaultContributionBranch({
|
|
661
|
+
options,
|
|
662
|
+
auth,
|
|
663
|
+
build,
|
|
664
|
+
});
|
|
665
|
+
console.log(
|
|
666
|
+
`Using your branch ${formatBuildTitle(branch)} for team project ${formatBuildTitle(build)}.`,
|
|
667
|
+
);
|
|
668
|
+
return branch;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function shouldUseContributionBranch(build) {
|
|
672
|
+
return (
|
|
673
|
+
build?.role === "collaborator" &&
|
|
674
|
+
build.canWrite !== true &&
|
|
675
|
+
!isContributionBranch(build)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export async function ensureDefaultContributionBranch({ options, auth, build }) {
|
|
680
|
+
const rootBuildId =
|
|
681
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
682
|
+
if (!rootBuildId) {
|
|
683
|
+
throw new Error("Could not resolve the team project for this branch.");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const result = await requestJson({
|
|
687
|
+
method: "POST",
|
|
688
|
+
url: `${options.apiUrl}/build/${rootBuildId}/contributions/default-branch`,
|
|
689
|
+
authToken: auth.token,
|
|
690
|
+
body: {},
|
|
691
|
+
timeoutMs: options.timeoutMs,
|
|
692
|
+
});
|
|
693
|
+
const branchId = Number(result.build?.id || 0);
|
|
694
|
+
if (!branchId) {
|
|
695
|
+
throw new Error("Twinkle did not return a contribution branch.");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return await loadBuildMetadata({
|
|
699
|
+
options,
|
|
700
|
+
auth,
|
|
701
|
+
buildId: branchId,
|
|
702
|
+
}).catch(() =>
|
|
703
|
+
normalizeContributionBranchBuild({
|
|
704
|
+
branch: result.build,
|
|
705
|
+
sourceBuild: build,
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function normalizeContributionBranchBuild({ branch, sourceBuild }) {
|
|
711
|
+
const branchId = Number(branch?.id || 0);
|
|
712
|
+
return {
|
|
713
|
+
...branch,
|
|
714
|
+
id: branchId,
|
|
715
|
+
title: branch?.title || `Branch ${branchId}`,
|
|
716
|
+
role: "owner",
|
|
717
|
+
ownerUsername:
|
|
718
|
+
branch?.ownerUsername ||
|
|
719
|
+
branch?.username ||
|
|
720
|
+
sourceBuild?.ownerUsername ||
|
|
721
|
+
null,
|
|
722
|
+
canWrite: true,
|
|
723
|
+
canPublish: false,
|
|
724
|
+
contributionStatus: branch?.contributionStatus || "draft",
|
|
725
|
+
contributionRootBuildId:
|
|
726
|
+
Number(branch?.contributionRootBuildId || sourceBuild?.id || 0) || null,
|
|
727
|
+
contributionContributorId:
|
|
728
|
+
Number(branch?.contributionContributorId || branch?.userId || 0) || null,
|
|
729
|
+
contributionBranchNumber:
|
|
730
|
+
Number(branch?.contributionBranchNumber || 0) || null,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export async function chooseProject({ builds }) {
|
|
735
|
+
if (!builds.length) {
|
|
736
|
+
throw new Error("No owned or team Twinkle builds were found.");
|
|
737
|
+
}
|
|
738
|
+
if (builds.length === 1) {
|
|
739
|
+
console.log(`Selected ${formatBuildTitle(builds[0])}.`);
|
|
740
|
+
return builds[0];
|
|
741
|
+
}
|
|
742
|
+
if (!input.isTTY || !output.isTTY) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
"Choose a project by running `lumine select <twinkle-build-url>`.",
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
printBuildList(builds);
|
|
749
|
+
const rl = readline.createInterface({ input, output });
|
|
750
|
+
try {
|
|
751
|
+
while (true) {
|
|
752
|
+
const answer = await rl.question("Choose a project number: ");
|
|
753
|
+
const index = Number(answer.trim());
|
|
754
|
+
if (Number.isInteger(index) && index >= 1 && index <= builds.length) {
|
|
755
|
+
return builds[index - 1];
|
|
756
|
+
}
|
|
757
|
+
console.log(`Enter a number from 1 to ${builds.length}.`);
|
|
758
|
+
}
|
|
759
|
+
} finally {
|
|
760
|
+
rl.close();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export async function pullBuildFiles({ options, auth, buildId }) {
|
|
765
|
+
const result = await loadBuildFiles({
|
|
766
|
+
options,
|
|
767
|
+
auth,
|
|
768
|
+
buildId,
|
|
769
|
+
includeContent: true,
|
|
770
|
+
});
|
|
771
|
+
const build = result.build || { id: buildId, title: `Build ${buildId}` };
|
|
772
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
773
|
+
const dir = path.resolve(options.dir || defaultWorkspaceDir(build));
|
|
774
|
+
await writeProjectFiles({ dir, files });
|
|
775
|
+
await writeAgentInstructions({ dir });
|
|
776
|
+
await writeSdkReference({ dir });
|
|
777
|
+
await writeProjectMetadata({
|
|
778
|
+
dir,
|
|
779
|
+
options,
|
|
780
|
+
build,
|
|
781
|
+
manifest: result.projectManifest || null,
|
|
782
|
+
pulledAt: new Date().toISOString(),
|
|
783
|
+
});
|
|
784
|
+
// Best-effort asset manifest so terminal agents know what media already
|
|
785
|
+
// exists (the workspace UI gives Lumine the same context). Never fail the
|
|
786
|
+
// pull over it.
|
|
787
|
+
let assetCount = null;
|
|
788
|
+
try {
|
|
789
|
+
const { token } = await mintBuildApiToken({
|
|
790
|
+
options,
|
|
791
|
+
auth,
|
|
792
|
+
buildId: Number(build.id || buildId),
|
|
793
|
+
scopes: ["files:read"],
|
|
794
|
+
});
|
|
795
|
+
const { assets, usage, projectAssets } = await fetchAllRuntimeAssets({
|
|
796
|
+
options,
|
|
797
|
+
auth,
|
|
798
|
+
buildId: Number(build.id || buildId),
|
|
799
|
+
buildApiToken: token,
|
|
800
|
+
});
|
|
801
|
+
await writeAssetsManifest({
|
|
802
|
+
dir,
|
|
803
|
+
buildId: Number(build.id || buildId),
|
|
804
|
+
assets,
|
|
805
|
+
usage,
|
|
806
|
+
projectAssets,
|
|
807
|
+
codeFiles: files,
|
|
808
|
+
});
|
|
809
|
+
assetCount = assets.length + (projectAssets?.length || 0);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
console.error(
|
|
812
|
+
`lumine: skipped ${ASSETS_METADATA_FILE} (${error?.message || error})`,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
build,
|
|
817
|
+
dir,
|
|
818
|
+
fileCount: files.length,
|
|
819
|
+
manifest: result.projectManifest || null,
|
|
820
|
+
assetCount,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export async function pullReferenceFiles({ options, auth, buildId }) {
|
|
825
|
+
const result = await loadOpenSourceBuildFiles({
|
|
826
|
+
options,
|
|
827
|
+
auth,
|
|
828
|
+
buildId,
|
|
829
|
+
includeContent: true,
|
|
830
|
+
});
|
|
831
|
+
const build = result.build || { id: buildId, title: `Build ${buildId}` };
|
|
832
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
833
|
+
const dir = path.resolve(options.dir || defaultReferenceDir(build));
|
|
834
|
+
await writeProjectFiles({ dir, files });
|
|
835
|
+
await writeReferenceInstructions({ dir });
|
|
836
|
+
await writeSdkReference({ dir });
|
|
837
|
+
await writeReferenceMetadata({
|
|
838
|
+
dir,
|
|
839
|
+
options,
|
|
840
|
+
build,
|
|
841
|
+
manifest: result.projectManifest || null,
|
|
842
|
+
reference: result.reference || {
|
|
843
|
+
readOnly: true,
|
|
844
|
+
forkable: true,
|
|
845
|
+
sourceBuildId: Number(build.id || buildId),
|
|
846
|
+
},
|
|
847
|
+
pulledAt: new Date().toISOString(),
|
|
848
|
+
});
|
|
849
|
+
return {
|
|
850
|
+
build,
|
|
851
|
+
dir,
|
|
852
|
+
fileCount: files.length,
|
|
853
|
+
manifest: result.projectManifest || null,
|
|
854
|
+
reference: result.reference || null,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Read-only checkout of a team project's MAIN workspace. Collaborators edit on
|
|
859
|
+
// their contribution branch; this exists so branch work can consult what main
|
|
860
|
+
// currently looks like without the pull auto-redirecting to the branch.
|
|
861
|
+
export async function pullMainBuildFiles({ options, auth, build }) {
|
|
862
|
+
const rootBuildId =
|
|
863
|
+
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
864
|
+
if (!rootBuildId) {
|
|
865
|
+
throw new Error("Could not resolve the team project for --main.");
|
|
866
|
+
}
|
|
867
|
+
const result = await loadBuildFiles({
|
|
868
|
+
options,
|
|
869
|
+
auth,
|
|
870
|
+
buildId: rootBuildId,
|
|
871
|
+
includeContent: true,
|
|
872
|
+
});
|
|
873
|
+
const rootBuild = result.build || { id: rootBuildId, title: `Build ${rootBuildId}` };
|
|
874
|
+
const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
|
|
875
|
+
// Never nest a main checkout inside another Lumine workspace (a later save
|
|
876
|
+
// there would upload it as project files) — default to a sibling instead.
|
|
877
|
+
// Running from a main checkout of this same build refreshes it in place,
|
|
878
|
+
// and the refresh prunes files main has deleted (a snapshot must not lie).
|
|
879
|
+
const enclosing = options.dir
|
|
880
|
+
? null
|
|
881
|
+
: await findLocalProjectMetadata(process.cwd());
|
|
882
|
+
const enclosingIsThisMain =
|
|
883
|
+
enclosing?.metadata?.mainCheckout === true &&
|
|
884
|
+
Number(enclosing.metadata.buildId || 0) === rootBuildId;
|
|
885
|
+
const dir = path.resolve(
|
|
886
|
+
options.dir ||
|
|
887
|
+
(enclosingIsThisMain
|
|
888
|
+
? enclosing.rootDir
|
|
889
|
+
: enclosing
|
|
890
|
+
? path.join(path.dirname(enclosing.rootDir), defaultMainCheckoutDir(rootBuild))
|
|
891
|
+
: defaultMainCheckoutDir(rootBuild)),
|
|
892
|
+
);
|
|
893
|
+
await writeProjectFiles({ dir, files });
|
|
894
|
+
if (files.some((file) => isIndexHtmlPath(String(file.path)))) {
|
|
895
|
+
await removeLocalProjectFilesNotIn({ dir, files });
|
|
896
|
+
}
|
|
897
|
+
await writeInstructionFiles({
|
|
898
|
+
dir,
|
|
899
|
+
marker: LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
|
|
900
|
+
content: LUMINE_MAIN_CHECKOUT_INSTRUCTIONS,
|
|
901
|
+
});
|
|
902
|
+
await writeSdkReference({ dir });
|
|
903
|
+
await writeMainCheckoutMetadata({
|
|
904
|
+
dir,
|
|
905
|
+
options,
|
|
906
|
+
build: rootBuild,
|
|
907
|
+
manifest: result.projectManifest || null,
|
|
908
|
+
pulledAt: new Date().toISOString(),
|
|
909
|
+
});
|
|
910
|
+
console.log(`Pulled main for ${formatBuildTitle(rootBuild)} (read-only).`);
|
|
911
|
+
console.log(`Pulled ${files.length} file${files.length === 1 ? "" : "s"} to ${dir}`);
|
|
912
|
+
console.log(
|
|
913
|
+
`Edits belong on your branch: \`lumine pull ${rootBuildId}\`, and \`lumine update-from-main\` brings main's changes into it.`,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export function printBuildList(builds) {
|
|
918
|
+
if (!builds.length) {
|
|
919
|
+
console.log("No owned or team Twinkle builds found.");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
console.log("Twinkle builds:");
|
|
923
|
+
builds.forEach((build, index) => {
|
|
924
|
+
console.log(`${index + 1}. ${formatBuildListItem(build)}`);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function printOpenSourceBuildList(builds, options) {
|
|
929
|
+
if (!builds.length) {
|
|
930
|
+
const searchText = options.searchQuery
|
|
931
|
+
? ` matching "${options.searchQuery}"`
|
|
932
|
+
: "";
|
|
933
|
+
console.log(`No public open-source Twinkle builds found${searchText}.`);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const searchText = options.searchQuery ? ` for "${options.searchQuery}"` : "";
|
|
937
|
+
console.log(`Public open-source Twinkle builds${searchText}:`);
|
|
938
|
+
builds.forEach((build, index) => {
|
|
939
|
+
console.log(`${index + 1}. ${formatOpenSourceBuildListItem(build)}`);
|
|
940
|
+
});
|
|
941
|
+
console.log("Reference: lumine reference <build-id>");
|
|
942
|
+
console.log("Fork: lumine fork <build-id>");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export function formatBuildListItem(build) {
|
|
946
|
+
const role =
|
|
947
|
+
build.role === "owner"
|
|
948
|
+
? "owned by you"
|
|
949
|
+
: `team project${build.ownerUsername ? ` with ${build.ownerUsername}` : ""}`;
|
|
950
|
+
const published = build.isPublic ? "public" : "private";
|
|
951
|
+
return `${formatBuildTitle(build)} - ${role}, ${published}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function formatOpenSourceBuildListItem(build) {
|
|
955
|
+
const owner = build.ownerUsername ? ` by ${build.ownerUsername}` : "";
|
|
956
|
+
const stats = [
|
|
957
|
+
`${Math.max(0, Number(build.forkCount || 0))} forks`,
|
|
958
|
+
`${Math.max(0, Number(build.viewCount || 0))} views`,
|
|
959
|
+
].join(", ");
|
|
960
|
+
const appUrl = build.appUrl ? ` - ${build.appUrl}` : "";
|
|
961
|
+
return `${formatBuildTitle(build)}${owner} - ${stats}${appUrl}`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export function formatBuildTitle(build) {
|
|
965
|
+
return `${build.title || `Build ${build.id}`} (#${build.id})`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export function isContributionBranch(build) {
|
|
969
|
+
return (
|
|
970
|
+
String(build?.contributionStatus || "none") !== "none" ||
|
|
971
|
+
Number(build?.contributionRootBuildId || 0) > 0 ||
|
|
972
|
+
Number(build?.contributionBranchNumber || 0) > 0
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export function resolveContributionActionBuildIds(build) {
|
|
977
|
+
const contributionBuildId = Number(build?.id || 0);
|
|
978
|
+
const rootBuildId = Number(build?.contributionRootBuildId || 0);
|
|
979
|
+
if (!contributionBuildId || !rootBuildId || !isContributionBranch(build)) {
|
|
980
|
+
throw new Error(
|
|
981
|
+
"Pass a branch URL such as https://www.twin-kle.com/build/884/4, or run this from a pulled branch workspace.",
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
return { rootBuildId, contributionBuildId };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export function printPullResult(result) {
|
|
988
|
+
const build = result.build || {};
|
|
989
|
+
const entryPath = result.manifest?.entryPath || "unknown";
|
|
990
|
+
console.log(`Selected ${formatBuildTitle(build)}.`);
|
|
991
|
+
console.log(
|
|
992
|
+
`Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
|
|
993
|
+
);
|
|
994
|
+
console.log(`Entry: ${entryPath}`);
|
|
995
|
+
console.log(`SDK reference: ${SDK_REFERENCE_FILE}`);
|
|
996
|
+
if (typeof result.assetCount === "number") {
|
|
997
|
+
console.log(
|
|
998
|
+
result.assetCount > 0
|
|
999
|
+
? `Assets: ${result.assetCount} uploaded (URLs in ${PROJECT_METADATA_DIR}/${ASSETS_METADATA_FILE})`
|
|
1000
|
+
: "Assets: none uploaded yet (add media with `lumine assets upload <file>`)",
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
console.log(`Next: cd ${shellQuote(result.dir)}`);
|
|
1004
|
+
if (build.canWrite === false) {
|
|
1005
|
+
console.log("This checkout is read-only for the current CLI login.");
|
|
1006
|
+
if (isContributionBranch(build)) {
|
|
1007
|
+
console.log("Review changes: lumine diff");
|
|
1008
|
+
if (build.role === "project_owner") {
|
|
1009
|
+
console.log("Owner actions: lumine merge, or lumine replace-main");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (result.fileCount === 0) {
|
|
1015
|
+
console.log("No project files yet. Create /index.html before your first save.");
|
|
1016
|
+
}
|
|
1017
|
+
console.log('Codex: codex "Read AGENTS.md, then make the requested change."');
|
|
1018
|
+
console.log(
|
|
1019
|
+
'Claude Code: claude "Read CLAUDE.md, then make the requested change."',
|
|
1020
|
+
);
|
|
1021
|
+
console.log('Save after edits: lumine save --summary "Describe the change"');
|
|
1022
|
+
if (isContributionBranch(build) && build.canPublish === false) {
|
|
1023
|
+
console.log("The project owner can merge or replace main from Twinkle.");
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log("Run `lumine check` or `lumine launch --save` when ready.");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function printNewBuildResult({ createResult, pullResult }) {
|
|
1030
|
+
const build = pullResult.build || createResult.build || {};
|
|
1031
|
+
console.log(`Created ${formatBuildTitle(build)}.`);
|
|
1032
|
+
printPullResult(pullResult);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export function printReferenceResult(result) {
|
|
1036
|
+
const build = result.build || {};
|
|
1037
|
+
const sourceBuildId =
|
|
1038
|
+
Number(result.reference?.sourceBuildId || 0) || Number(build.id || 0);
|
|
1039
|
+
const entryPath = result.manifest?.entryPath || "unknown";
|
|
1040
|
+
console.log(`Referenced ${formatBuildTitle(build)}.`);
|
|
1041
|
+
console.log(
|
|
1042
|
+
`Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
|
|
1043
|
+
);
|
|
1044
|
+
console.log(`Entry: ${entryPath}`);
|
|
1045
|
+
console.log("Mode: read-only reference");
|
|
1046
|
+
console.log(`Next: cd ${shellQuote(result.dir)}`);
|
|
1047
|
+
if (sourceBuildId) {
|
|
1048
|
+
console.log(`Start from this app: lumine fork ${sourceBuildId}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export function printForkResult({ forkResult, pullResult }) {
|
|
1053
|
+
const build = pullResult.build || forkResult.build || {};
|
|
1054
|
+
const sourceBuildId =
|
|
1055
|
+
Number(forkResult.sourceBuild?.id || 0) ||
|
|
1056
|
+
Number(forkResult.sourceBuild?.contentId || 0) ||
|
|
1057
|
+
0;
|
|
1058
|
+
console.log(
|
|
1059
|
+
forkResult.alreadyExists
|
|
1060
|
+
? `Using your existing fork ${formatBuildTitle(build)}.`
|
|
1061
|
+
: `Forked Build ${sourceBuildId || "source"} into ${formatBuildTitle(
|
|
1062
|
+
build,
|
|
1063
|
+
)}.`,
|
|
1064
|
+
);
|
|
1065
|
+
printPullResult(pullResult);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export function printSaveResult({ result, build, dir, files }) {
|
|
1069
|
+
const entryPath = result.projectManifest?.entryPath || "unknown";
|
|
1070
|
+
const version = result.artifactVersion?.versionNumber
|
|
1071
|
+
? ` v${result.artifactVersion.versionNumber}`
|
|
1072
|
+
: "";
|
|
1073
|
+
const releaseState = result.releaseStatus?.state || "unknown";
|
|
1074
|
+
console.log(`Saved ${formatBuildTitle(build)}${version}.`);
|
|
1075
|
+
console.log(
|
|
1076
|
+
`Uploaded ${files.length} file${files.length === 1 ? "" : "s"} from ${dir}`,
|
|
1077
|
+
);
|
|
1078
|
+
console.log(`Entry: ${entryPath}`);
|
|
1079
|
+
console.log(`Release status: ${releaseState}`);
|
|
1080
|
+
if (isContributionBranch(build) && build.canPublish === false) {
|
|
1081
|
+
console.log(
|
|
1082
|
+
"Next: the project owner can merge or replace main from Twinkle.",
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
console.log(
|
|
1086
|
+
"Next: run `lumine launch` to publish, or `lumine save --publish` next time.",
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export function printContributionDiff({ result, build }) {
|
|
1092
|
+
const summary = result.diff?.summary || {};
|
|
1093
|
+
const files = Array.isArray(result.diff?.changedFiles)
|
|
1094
|
+
? result.diff.changedFiles
|
|
1095
|
+
: [];
|
|
1096
|
+
const branchNumber = Number(build.contributionBranchNumber || 0) || 0;
|
|
1097
|
+
const branchLabel = branchNumber ? `branch ${branchNumber}` : `branch #${build.id}`;
|
|
1098
|
+
console.log(`Diff for ${branchLabel}:`);
|
|
1099
|
+
console.log(
|
|
1100
|
+
`- total=${summary.total ?? files.length} added=${summary.added ?? 0} ` +
|
|
1101
|
+
`updated=${summary.updated ?? 0} deleted=${summary.deleted ?? 0}`,
|
|
1102
|
+
);
|
|
1103
|
+
if (result.rootDrifted) {
|
|
1104
|
+
console.log("- main changed after this branch was created");
|
|
1105
|
+
}
|
|
1106
|
+
if (!files.length) {
|
|
1107
|
+
console.log("No file changes.");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
for (const file of files) {
|
|
1111
|
+
const mergeStatus = file.mergeStatus ? ` (${file.mergeStatus})` : "";
|
|
1112
|
+
console.log(`- ${file.status || "changed"} ${file.path}${mergeStatus}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function printUpdateFromMainResult({ result, build, dir, mergedFiles }) {
|
|
1117
|
+
const branchNumber = Number(build?.contributionBranchNumber || 0) || 0;
|
|
1118
|
+
const branchLabel = branchNumber
|
|
1119
|
+
? `branch ${branchNumber}`
|
|
1120
|
+
: `branch #${build?.id}`;
|
|
1121
|
+
const autoMerged = Array.isArray(result.autoMergedPaths)
|
|
1122
|
+
? result.autoMergedPaths
|
|
1123
|
+
: [];
|
|
1124
|
+
const conflicts = Array.isArray(result.conflicts) ? result.conflicts : [];
|
|
1125
|
+
console.log(
|
|
1126
|
+
`Updated ${branchLabel} from main (${mergedFiles.length} file${mergedFiles.length === 1 ? "" : "s"}).`,
|
|
1127
|
+
);
|
|
1128
|
+
if (autoMerged.length > 0) {
|
|
1129
|
+
console.log(`Auto-merged: ${autoMerged.join(", ")}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (conflicts.length > 0) {
|
|
1132
|
+
const conflictPaths = conflicts.map((conflict) =>
|
|
1133
|
+
typeof conflict === "string" ? conflict : conflict?.path || "unknown",
|
|
1134
|
+
);
|
|
1135
|
+
console.log(`CONFLICTS (markers written): ${conflictPaths.join(", ")}`);
|
|
1136
|
+
console.log(
|
|
1137
|
+
dir
|
|
1138
|
+
? "Resolve the <<<<<<< / >>>>>>> markers in the files above, then run `lumine save`."
|
|
1139
|
+
: "Pull the branch, resolve the <<<<<<< / >>>>>>> markers, then run `lumine save`.",
|
|
1140
|
+
);
|
|
1141
|
+
} else if (!dir) {
|
|
1142
|
+
console.log(
|
|
1143
|
+
"The branch was updated on Twinkle. Run `lumine pull` to refresh your local workspace.",
|
|
1144
|
+
);
|
|
1145
|
+
} else {
|
|
1146
|
+
console.log(`Local workspace updated: ${dir}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export function printContributionActionResult({
|
|
1151
|
+
action,
|
|
1152
|
+
result,
|
|
1153
|
+
rootBuildId,
|
|
1154
|
+
contributionBuildId,
|
|
1155
|
+
}) {
|
|
1156
|
+
if (!result?.success) {
|
|
1157
|
+
console.log(result?.error || "Branch action did not complete.");
|
|
1158
|
+
process.exitCode = 1;
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const projectFiles = Array.isArray(result.projectFiles)
|
|
1162
|
+
? result.projectFiles
|
|
1163
|
+
: [];
|
|
1164
|
+
console.log(`${action} branch #${contributionBuildId} for Build #${rootBuildId}.`);
|
|
1165
|
+
if (projectFiles.length > 0) {
|
|
1166
|
+
console.log(
|
|
1167
|
+
`Main now has ${projectFiles.length} project file${projectFiles.length === 1 ? "" : "s"}.`,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (result.mergeConflictsWritten || result.conflicts?.length > 0) {
|
|
1171
|
+
console.log("Merge wrote conflict markers. Resolve them in the Build workspace.");
|
|
1172
|
+
}
|
|
1173
|
+
console.log(`Main workspace: ${rootBuildId}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function parseArgs(args) {
|
|
1177
|
+
const firstArg = args[0] || "";
|
|
1178
|
+
const firstArgIsCommand =
|
|
1179
|
+
firstArg && !firstArg.startsWith("--") && COMMANDS.has(firstArg);
|
|
1180
|
+
const firstArgLooksLikeTarget =
|
|
1181
|
+
firstArg && !firstArg.startsWith("--") && resolveBuildId(firstArg) > 0;
|
|
1182
|
+
const command = firstArgIsCommand
|
|
1183
|
+
? firstArg
|
|
1184
|
+
: !firstArg || firstArg.startsWith("--") || firstArgLooksLikeTarget
|
|
1185
|
+
? "workspace"
|
|
1186
|
+
: "help";
|
|
1187
|
+
const rest = command === "workspace" ? args : args.slice(1);
|
|
1188
|
+
const raw = {};
|
|
1189
|
+
const positional = [];
|
|
1190
|
+
const booleanFlags = new Set([
|
|
1191
|
+
"noOpen",
|
|
1192
|
+
"open",
|
|
1193
|
+
"noDescription",
|
|
1194
|
+
"publish",
|
|
1195
|
+
"save",
|
|
1196
|
+
"noUpdateCheck",
|
|
1197
|
+
"allowWrite",
|
|
1198
|
+
"main",
|
|
1199
|
+
"yes",
|
|
1200
|
+
]);
|
|
1201
|
+
|
|
1202
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1203
|
+
const arg = rest[i];
|
|
1204
|
+
if (arg === "--help" || arg === "-h") {
|
|
1205
|
+
raw.help = true;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (!arg.startsWith("--")) {
|
|
1209
|
+
positional.push(arg);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const [key, inlineValue] = arg.slice(2).split("=", 2);
|
|
1213
|
+
const camelKey = toCamelCase(key);
|
|
1214
|
+
if (inlineValue !== undefined) {
|
|
1215
|
+
raw[camelKey] = inlineValue;
|
|
1216
|
+
} else if (booleanFlags.has(camelKey)) {
|
|
1217
|
+
raw[camelKey] = true;
|
|
1218
|
+
} else {
|
|
1219
|
+
raw[camelKey] = rest[i + 1] ?? "";
|
|
1220
|
+
i += 1;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
command,
|
|
1226
|
+
positional,
|
|
1227
|
+
repeat: Math.min(Math.max(Math.floor(Number(raw.repeat) || 1), 1), 20),
|
|
1228
|
+
allowWrite: parseBoolean(raw.allowWrite, false),
|
|
1229
|
+
assumeYes: parseBoolean(raw.yes, false),
|
|
1230
|
+
sdkPath: raw.path ? String(raw.path) : "",
|
|
1231
|
+
sdkScopes: String(raw.scopes || "")
|
|
1232
|
+
.split(",")
|
|
1233
|
+
.map((scope) => scope.trim())
|
|
1234
|
+
.filter(Boolean),
|
|
1235
|
+
buildIdFlag: raw.build ? String(raw.build) : "",
|
|
1236
|
+
target: raw.url || raw.target || positional[0] || "",
|
|
1237
|
+
title:
|
|
1238
|
+
String(
|
|
1239
|
+
raw.title ||
|
|
1240
|
+
(command === "new" ? positional.join(" ") : ""),
|
|
1241
|
+
).trim() || "",
|
|
1242
|
+
description:
|
|
1243
|
+
Object.prototype.hasOwnProperty.call(raw, "description")
|
|
1244
|
+
? String(raw.description || "").trim()
|
|
1245
|
+
: null,
|
|
1246
|
+
descriptionProvided: Object.prototype.hasOwnProperty.call(
|
|
1247
|
+
raw,
|
|
1248
|
+
"description",
|
|
1249
|
+
),
|
|
1250
|
+
noDescription: parseBoolean(raw.noDescription, false),
|
|
1251
|
+
searchQuery:
|
|
1252
|
+
String(
|
|
1253
|
+
raw.search ||
|
|
1254
|
+
raw.query ||
|
|
1255
|
+
(command === "explore" ? positional.join(" ") : ""),
|
|
1256
|
+
).trim() || "",
|
|
1257
|
+
sort: normalizeOpenSourceSort(raw.sort),
|
|
1258
|
+
apiUrl: trimTrailingSlash(
|
|
1259
|
+
String(raw.apiUrl || process.env.TWINKLE_API_URL || DEFAULT_API_URL),
|
|
1260
|
+
),
|
|
1261
|
+
siteUrl: trimTrailingSlash(
|
|
1262
|
+
String(raw.siteUrl || process.env.TWINKLE_SITE_URL || DEFAULT_SITE_URL),
|
|
1263
|
+
),
|
|
1264
|
+
npmRegistryUrl: trimTrailingSlash(
|
|
1265
|
+
String(
|
|
1266
|
+
raw.npmRegistryUrl ||
|
|
1267
|
+
process.env.LUMINE_NPM_REGISTRY_URL ||
|
|
1268
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
1269
|
+
),
|
|
1270
|
+
),
|
|
1271
|
+
authFile: String(
|
|
1272
|
+
raw.authFile || process.env.TWINKLE_CLI_AUTH_FILE || DEFAULT_AUTH_FILE,
|
|
1273
|
+
),
|
|
1274
|
+
authToken:
|
|
1275
|
+
String(raw.authToken || process.env.TWINKLE_AUTH_TOKEN || "").trim() ||
|
|
1276
|
+
null,
|
|
1277
|
+
clientName: String(raw.clientName || "Lumine CLI").slice(0, 120),
|
|
1278
|
+
dir: raw.dir ? String(raw.dir) : "",
|
|
1279
|
+
pullMain: parseBoolean(raw.main, false),
|
|
1280
|
+
summary: raw.summary ? String(raw.summary) : "",
|
|
1281
|
+
publish: parseBoolean(raw.publish, false),
|
|
1282
|
+
saveFirst: parseBoolean(raw.save, false),
|
|
1283
|
+
limit: Math.min(
|
|
1284
|
+
Math.max(
|
|
1285
|
+
Number(raw.limit || process.env.TWINKLE_PROJECT_LIMIT) ||
|
|
1286
|
+
DEFAULT_PROJECT_LIMIT,
|
|
1287
|
+
1,
|
|
1288
|
+
),
|
|
1289
|
+
100,
|
|
1290
|
+
),
|
|
1291
|
+
openBrowser: parseBoolean(raw.noOpen, false)
|
|
1292
|
+
? false
|
|
1293
|
+
: parseBoolean(raw.open, true),
|
|
1294
|
+
updateCheck: parseBoolean(raw.noUpdateCheck, false) ? false : true,
|
|
1295
|
+
timeoutMs: Math.max(
|
|
1296
|
+
Number(raw.timeoutMs || process.env.TWINKLE_TIMEOUT_MS) ||
|
|
1297
|
+
DEFAULT_TIMEOUT_MS,
|
|
1298
|
+
1000,
|
|
1299
|
+
),
|
|
1300
|
+
help: !!raw.help || command === "help",
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
export async function resolveRequiredBuildIdOrSelected(
|
|
1305
|
+
options,
|
|
1306
|
+
auth,
|
|
1307
|
+
{ localProject = null } = {},
|
|
1308
|
+
) {
|
|
1309
|
+
const targetReference = resolveBuildReference(options.target);
|
|
1310
|
+
if (targetReference.buildId > 0) {
|
|
1311
|
+
return await resolveBuildReferenceBuildId({
|
|
1312
|
+
options,
|
|
1313
|
+
auth,
|
|
1314
|
+
reference: targetReference,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
const resolvedLocalProject =
|
|
1318
|
+
localProject ||
|
|
1319
|
+
(await findLocalProjectMetadata(
|
|
1320
|
+
path.resolve(options.dir || process.cwd()),
|
|
1321
|
+
));
|
|
1322
|
+
// A `pull --main` checkout is read-only but still knows its root build id.
|
|
1323
|
+
// Read-only commands run from it (check, diff, a refreshing `pull --main`)
|
|
1324
|
+
// resolve that id instead of bouncing off the reference/fork error — but
|
|
1325
|
+
// mutating commands (launch would PUBLISH main, save, merge, …) stay blocked
|
|
1326
|
+
// with main-checkout-specific guidance.
|
|
1327
|
+
if (resolvedLocalProject?.metadata?.mainCheckout === true) {
|
|
1328
|
+
const mainBuildId =
|
|
1329
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1330
|
+
Number(resolvedLocalProject.metadata.build?.id || 0);
|
|
1331
|
+
if (!MAIN_CHECKOUT_READONLY_COMMANDS.has(options.command)) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`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.`,
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (mainBuildId > 0) return mainBuildId;
|
|
1337
|
+
}
|
|
1338
|
+
if (
|
|
1339
|
+
resolvedLocalProject?.metadata &&
|
|
1340
|
+
isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
|
|
1341
|
+
) {
|
|
1342
|
+
const sourceBuildId =
|
|
1343
|
+
Number(resolvedLocalProject.metadata.reference?.sourceBuildId || 0) ||
|
|
1344
|
+
Number(resolvedLocalProject.metadata.buildId || 0) ||
|
|
1345
|
+
Number(resolvedLocalProject.metadata.build?.id || 0) ||
|
|
1346
|
+
0;
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace, or pass an explicit Build URL.`,
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const localBuildId = Number(resolvedLocalProject?.metadata?.buildId || 0);
|
|
1352
|
+
if (localBuildId > 0) return localBuildId;
|
|
1353
|
+
const selectedBuildId = Number(auth?.selectedBuildId || 0);
|
|
1354
|
+
if (selectedBuildId > 0) return selectedBuildId;
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
"Choose a project with `lumine select`, run `lumine`, or pass a Twinkle build URL.",
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
export async function resolveNewBuildTitle(options) {
|
|
1361
|
+
const title = String(options.title || "").trim();
|
|
1362
|
+
if (title) return title;
|
|
1363
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1364
|
+
throw new Error('Pass a title: `lumine new "My Build"` or `lumine new --title "My Build"`.');
|
|
1365
|
+
}
|
|
1366
|
+
const rl = readline.createInterface({ input, output });
|
|
1367
|
+
try {
|
|
1368
|
+
const answer = await rl.question("Build title: ");
|
|
1369
|
+
const normalized = String(answer || "").trim();
|
|
1370
|
+
if (!normalized) {
|
|
1371
|
+
throw new Error("Title is required.");
|
|
1372
|
+
}
|
|
1373
|
+
return normalized;
|
|
1374
|
+
} finally {
|
|
1375
|
+
rl.close();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export async function resolveNewBuildDescription(options) {
|
|
1380
|
+
if (options.noDescription) return null;
|
|
1381
|
+
if (options.descriptionProvided) {
|
|
1382
|
+
return String(options.description || "").trim() || null;
|
|
1383
|
+
}
|
|
1384
|
+
if (!input.isTTY || !output.isTTY) return null;
|
|
1385
|
+
const rl = readline.createInterface({ input, output });
|
|
1386
|
+
try {
|
|
1387
|
+
const answer = await rl.question("Description (optional): ");
|
|
1388
|
+
return String(answer || "").trim() || null;
|
|
1389
|
+
} finally {
|
|
1390
|
+
rl.close();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
export async function resolveBuildReferenceBuildId({ options, auth, reference }) {
|
|
1395
|
+
if (reference.branchNumber > 0) {
|
|
1396
|
+
const build = await resolveBranchBuild({
|
|
1397
|
+
options,
|
|
1398
|
+
auth,
|
|
1399
|
+
rootBuildId: reference.buildId,
|
|
1400
|
+
branchNumber: reference.branchNumber,
|
|
1401
|
+
});
|
|
1402
|
+
return Number(build.id || 0);
|
|
1403
|
+
}
|
|
1404
|
+
return Number(reference.buildId || 0);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
export function normalizeOpenSourceSort(value) {
|
|
1408
|
+
const normalized = String(value || "")
|
|
1409
|
+
.trim()
|
|
1410
|
+
.toLowerCase();
|
|
1411
|
+
if (["recent", "popular", "forks"].includes(normalized)) return normalized;
|
|
1412
|
+
return "forks";
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
export function printHelp() {
|
|
1416
|
+
console.log(`Usage:
|
|
1417
|
+
lumine
|
|
1418
|
+
lumine login
|
|
1419
|
+
lumine whoami
|
|
1420
|
+
lumine logout
|
|
1421
|
+
lumine new [title]
|
|
1422
|
+
lumine projects
|
|
1423
|
+
lumine explore [search terms]
|
|
1424
|
+
lumine select [twinkle-build-url]
|
|
1425
|
+
lumine pull [twinkle-build-url]
|
|
1426
|
+
lumine pull [twinkle-build-url] --main
|
|
1427
|
+
lumine reference <twinkle-build-url>
|
|
1428
|
+
lumine fork <twinkle-build-url>
|
|
1429
|
+
lumine diff <twinkle-branch-url>
|
|
1430
|
+
lumine merge <twinkle-branch-url>
|
|
1431
|
+
lumine replace-main <twinkle-branch-url>
|
|
1432
|
+
lumine update-from-main [twinkle-branch-url]
|
|
1433
|
+
lumine save
|
|
1434
|
+
lumine check [twinkle-build-url]
|
|
1435
|
+
lumine launch [twinkle-build-url]
|
|
1436
|
+
lumine sdk list
|
|
1437
|
+
lumine sdk call <namespace.method> [jsonArgs]
|
|
1438
|
+
lumine assets [list]
|
|
1439
|
+
lumine assets upload <file...>
|
|
1440
|
+
lumine assets delete <assetId>
|
|
1441
|
+
lumine assets prune [--yes]
|
|
1442
|
+
|
|
1443
|
+
Examples:
|
|
1444
|
+
npx @stage5/lumine@latest
|
|
1445
|
+
npx @stage5/lumine@latest login
|
|
1446
|
+
npx @stage5/lumine@latest new "Daily Reflection App"
|
|
1447
|
+
npx @stage5/lumine@latest new --title "Daily Reflection App" --description "Private journal with streaks"
|
|
1448
|
+
npx @stage5/lumine@latest explore --sort forks
|
|
1449
|
+
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
1450
|
+
npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
|
|
1451
|
+
npx @stage5/lumine@latest diff https://www.twin-kle.com/build/884/4
|
|
1452
|
+
npx @stage5/lumine@latest merge https://www.twin-kle.com/build/884/4
|
|
1453
|
+
npx @stage5/lumine@latest pull 884 --main
|
|
1454
|
+
npx @stage5/lumine@latest update-from-main
|
|
1455
|
+
npx @stage5/lumine@latest pull
|
|
1456
|
+
npx @stage5/lumine@latest save
|
|
1457
|
+
npx @stage5/lumine@latest save --publish
|
|
1458
|
+
npx @stage5/lumine@latest launch --save
|
|
1459
|
+
npx @stage5/lumine@latest launch https://www.twin-kle.com/app/123
|
|
1460
|
+
npx @stage5/lumine@latest sdk call aiStories.chapters '{"limit": 5}'
|
|
1461
|
+
npx @stage5/lumine@latest sdk call privateDb.get '{"key": "prefs"}' --build 1374
|
|
1462
|
+
npx @stage5/lumine@latest sdk call aiStories.list '{"difficulty": 1}' --repeat 5
|
|
1463
|
+
npx @stage5/lumine@latest assets upload art/hero.png sounds/theme.mp3
|
|
1464
|
+
npx @stage5/lumine@latest assets list --build 917
|
|
1465
|
+
|
|
1466
|
+
Options:
|
|
1467
|
+
--api-url <url> Twinkle API origin
|
|
1468
|
+
--site-url <url> Twinkle website origin
|
|
1469
|
+
--auth-file <path> Saved login path
|
|
1470
|
+
--auth-token <token> Override saved login
|
|
1471
|
+
--dir <path> Directory for pulled project files
|
|
1472
|
+
--main With pull: read-only checkout of the team project's main
|
|
1473
|
+
--title <text> New Build title
|
|
1474
|
+
--description <text> Optional New Build description
|
|
1475
|
+
--no-description Skip the New Build description prompt
|
|
1476
|
+
--summary <text> Save summary
|
|
1477
|
+
--search <text> Search public open-source Builds
|
|
1478
|
+
--sort <sort> Sort open-source Builds: forks, popular, recent
|
|
1479
|
+
--publish Publish after saving
|
|
1480
|
+
--save Save local files before launch
|
|
1481
|
+
--limit <number> Number of projects to show
|
|
1482
|
+
--no-update-check Skip the npm latest-version check
|
|
1483
|
+
--no-open Print the approval URL without opening a browser
|
|
1484
|
+
--build <id> Build id for sdk/assets calls outside a workspace
|
|
1485
|
+
--repeat <n> Repeat an sdk call (1-20) and print latency stats
|
|
1486
|
+
--allow-write Permit sdk methods that mutate app data
|
|
1487
|
+
--path <api/...> Call an sdk endpoint not in the curated list
|
|
1488
|
+
--scopes <a,b> Override requested build API token scopes
|
|
1489
|
+
--yes Skip the confirmation prompt (assets prune)
|
|
1490
|
+
`);
|
|
1491
|
+
}
|