@stage5/lumine 0.2.37 → 0.2.38

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/commands.js CHANGED
@@ -908,7 +908,7 @@ export async function save(options) {
908
908
  buildId = Number(build?.id || buildId);
909
909
  const dir = resolveProjectDirForSave({ options, localProject });
910
910
  const files = await collectProjectFiles(dir);
911
- assertProjectFilesWithinLimits(files);
911
+ assertProjectFilesWithinLimits(files, build?.projectLimits);
912
912
  // Only claim a base snapshot when this workspace was pulled from the same
913
913
  // build we are saving to. Missing filesHash used to mean "unguarded save",
914
914
  // which let stale checkouts silently rewind newer server versions — refuse
@@ -975,12 +975,25 @@ export async function save(options) {
975
975
  }
976
976
  throw error;
977
977
  }
978
- build = result.build ||
979
- build ||
980
- (await loadBuildMetadata({ options, auth, buildId }).catch(() => null)) || {
978
+ build = result.build ? { ...build, ...result.build } : build;
979
+ if (result.copilotPolicy?.limits) {
980
+ build = {
981
+ ...build,
982
+ projectLimits: {
983
+ maxProjectBytes: result.copilotPolicy.limits.maxProjectBytes,
984
+ maxFilesPerProject: result.copilotPolicy.limits.maxFilesPerProject,
985
+ maxFileLines: result.copilotPolicy.limits.maxFileLines,
986
+ },
987
+ };
988
+ }
989
+ if (!build) {
990
+ build = (await loadBuildMetadata({ options, auth, buildId }).catch(
991
+ () => null,
992
+ )) || {
981
993
  id: buildId,
982
994
  title: `Build ${buildId}`,
983
995
  };
996
+ }
984
997
  await saveSelectedBuild({ options, auth, build });
985
998
  await writeProjectMetadata({
986
999
  dir,
@@ -1010,9 +1023,10 @@ export async function save(options) {
1010
1023
  }
1011
1024
 
1012
1025
  export async function check(options) {
1013
- await reportLocalProjectFindings(options);
1014
1026
  const auth = await resolveAuth(options);
1015
1027
  const buildId = await resolveRequiredBuildIdOrSelected(options, auth);
1028
+ const canonicalBuild = await loadBuildMetadata({ options, auth, buildId });
1029
+ await reportLocalProjectFindings(options, canonicalBuild?.projectLimits);
1016
1030
  const result = await requestJson({
1017
1031
  url: `${options.apiUrl}/cli/build/${buildId}/launch-check`,
1018
1032
  authToken: auth.token,
@@ -1022,9 +1036,13 @@ export async function check(options) {
1022
1036
  if (!result.ok) process.exitCode = 1;
1023
1037
  }
1024
1038
 
1025
- // Local, network-free half of `lumine check`: validate the workspace against
1026
- // the platform's project-file rules so agents see violations before saving.
1027
- export async function reportLocalProjectFindings(options) {
1039
+ // Local half of `lumine check`: validate the workspace against the canonical
1040
+ // project limits fetched by check(), so a newly approved allowance works
1041
+ // immediately without requiring another pull just to refresh local metadata.
1042
+ export async function reportLocalProjectFindings(
1043
+ options,
1044
+ canonicalProjectLimits,
1045
+ ) {
1028
1046
  const localProject = await findLocalProjectMetadata(
1029
1047
  path.resolve(options.dir || process.cwd()),
1030
1048
  );
@@ -1037,7 +1055,10 @@ export async function reportLocalProjectFindings(options) {
1037
1055
  process.exitCode = 1;
1038
1056
  return;
1039
1057
  }
1040
- const { errors, warnings } = collectProjectLimitFindings(files);
1058
+ const { errors, warnings } = collectProjectLimitFindings(
1059
+ files,
1060
+ canonicalProjectLimits || localProject?.metadata?.build?.projectLimits,
1061
+ );
1041
1062
  for (const warning of warnings) {
1042
1063
  console.error(`Local check warning: ${warning}`);
1043
1064
  }
@@ -1171,10 +1192,14 @@ export async function resolveBuildForSave({
1171
1192
  const localBuild = localProject?.metadata?.build;
1172
1193
  const localBuildId =
1173
1194
  Number(localBuild?.id || 0) || Number(localProject?.metadata?.buildId || 0);
1195
+ // Save is already an online operation. Refresh the server-owned Build
1196
+ // metadata before enforcing permissions and project limits so a newly
1197
+ // approved Main allowance (inherited by branches) works without re-pulling.
1198
+ const canonicalBuild = await loadBuildMetadata({ options, auth, buildId });
1174
1199
  const build =
1175
1200
  localBuild && localBuildId === Number(buildId)
1176
- ? { ...localBuild, id: buildId }
1177
- : await loadBuildMetadata({ options, auth, buildId });
1201
+ ? { ...localBuild, ...canonicalBuild, id: buildId }
1202
+ : canonicalBuild;
1178
1203
  return await resolveEditableWorkspaceBuild({
1179
1204
  options,
1180
1205
  auth,
@@ -1281,6 +1306,7 @@ export function normalizeContributionBranchBuild({ branch, sourceBuild }) {
1281
1306
  Number(branch?.contributionContributorId || branch?.userId || 0) || null,
1282
1307
  contributionBranchNumber:
1283
1308
  Number(branch?.contributionBranchNumber || 0) || null,
1309
+ projectLimits: branch?.projectLimits || sourceBuild?.projectLimits || null,
1284
1310
  };
1285
1311
  }
1286
1312
 
package/lib/constants.js CHANGED
@@ -69,14 +69,12 @@ export const ASSET_MIME_BY_EXTENSION = {
69
69
  ".bin": "application/octet-stream",
70
70
  ".drc": "application/octet-stream",
71
71
  };
72
- // Mirrors the platform's project-file limits. File count and effective-line
73
- // limits are hardcoded server constants; the total-byte limit is
74
- // env-overridable server-side, so the CLI only warns on it.
72
+ // Default platform project-file limits. A pulled workspace can carry larger
73
+ // canonical per-project limits approved on Main in lumine-project.json.
75
74
  export const PROJECT_MAX_FILES = 100;
76
75
  export const PROJECT_MAX_EFFECTIVE_FILE_LINES = 500;
77
76
  export const PROJECT_EFFECTIVE_LINE_MAX_COLUMNS = 160;
78
77
  export const PROJECT_MAX_TOTAL_BYTES_DEFAULT = 1024 * 1024;
79
- export const PROJECT_WARN_TOTAL_BYTES = 500 * 1024;
80
78
  // Matches asset references in project files: absolute CloudFront URLs and
81
79
  // relative /attachments/build-runtime|optimized/... paths.
82
80
  export const RUNTIME_ASSET_REFERENCE_PATTERN =
@@ -233,10 +231,12 @@ lumine save --summary "Describe the change"
233
231
 
234
232
  ## Project File Limits
235
233
 
236
- - Max ${PROJECT_MAX_FILES} files per project; each file max ${PROJECT_MAX_EFFECTIVE_FILE_LINES} effective lines
234
+ - Default max ${PROJECT_MAX_FILES} files and ${Math.floor(PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024)} KB per project. Read
235
+ \`.twinkle/lumine-project.json\` for this project's canonical \`projectLimits\`;
236
+ an approved Main project can allow up to 500 files and 5 MB, inherited by branches.
237
+ - Each file max ${PROJECT_MAX_EFFECTIVE_FILE_LINES} effective lines
237
238
  (a physical line counts once per ${PROJECT_EFFECTIVE_LINE_MAX_COLUMNS} characters, so minified one-liners
238
- blow the limit). Default total project size cap ${Math.floor(PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024)} KB,
239
- with a soft warning from ${Math.floor(PROJECT_WARN_TOTAL_BYTES / 1024)} KB.
239
+ blow the limit).
240
240
  - Plan file splits BEFORE writing code; \`lumine check\` validates the local
241
241
  workspace against these limits without saving, and \`lumine save\` fails fast
242
242
  locally listing every violation.
package/lib/workspace.js CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  PROJECT_MAX_EFFECTIVE_FILE_LINES,
16
16
  PROJECT_MAX_FILES,
17
17
  PROJECT_MAX_TOTAL_BYTES_DEFAULT,
18
- PROJECT_WARN_TOTAL_BYTES,
19
18
  PROJECT_METADATA_DIR,
20
19
  PROJECT_METADATA_FILE,
21
20
  SDK_REFERENCE_FALLBACK,
@@ -342,16 +341,30 @@ export function countEffectiveLines(value) {
342
341
  );
343
342
  }
344
343
 
345
- // Local mirror of the server's project limits so violations fail fast with
346
- // the full list (the server stops at the first). File count and per-file
347
- // effective lines are hard server constants -> errors; the total-byte limit
348
- // is env-overridable server-side -> warning only, the server stays the gate.
349
- export function collectProjectLimitFindings(files) {
344
+ // Local mirror of the canonical per-project limits returned by the server so
345
+ // violations fail fast with the full list (the server stops at the first).
346
+ export function collectProjectLimitFindings(files, projectLimits = {}) {
350
347
  const errors = [];
351
348
  const warnings = [];
352
- if (files.length > PROJECT_MAX_FILES) {
349
+ const canonicalMaxFiles = Math.floor(
350
+ Number(projectLimits?.maxFilesPerProject || 0),
351
+ );
352
+ const canonicalMaxProjectBytes = Math.floor(
353
+ Number(projectLimits?.maxProjectBytes || 0),
354
+ );
355
+ const maxFilesPerProject =
356
+ canonicalMaxFiles > 0 ? canonicalMaxFiles : PROJECT_MAX_FILES;
357
+ const maxProjectBytes =
358
+ canonicalMaxProjectBytes > 0
359
+ ? canonicalMaxProjectBytes
360
+ : PROJECT_MAX_TOTAL_BYTES_DEFAULT;
361
+ const warnProjectBytes = Math.min(
362
+ Math.floor(maxProjectBytes * 0.8),
363
+ maxProjectBytes - 1,
364
+ );
365
+ if (files.length > maxFilesPerProject) {
353
366
  errors.push(
354
- `Project exceeds the ${PROJECT_MAX_FILES}-file limit (${files.length} files). Split or consolidate files before saving.`,
367
+ `Project exceeds the ${maxFilesPerProject}-file limit (${files.length} files). Split or consolidate files before saving.`,
355
368
  );
356
369
  }
357
370
  for (const file of files) {
@@ -369,26 +382,23 @@ export function collectProjectLimitFindings(files) {
369
382
  total + Buffer.byteLength(String(file.content || ""), "utf8"),
370
383
  0,
371
384
  );
372
- if (totalBytes > PROJECT_MAX_TOTAL_BYTES_DEFAULT) {
373
- warnings.push(
374
- `Project files total ${formatBytes(totalBytes)}, above the default ${Math.floor(
375
- PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024,
376
- )} KB project limit — the server may reject this save.`,
385
+ if (totalBytes > maxProjectBytes) {
386
+ errors.push(
387
+ `Project files total ${formatBytes(totalBytes)}, above this project's ${formatBytes(maxProjectBytes)} limit.`,
377
388
  );
378
- } else if (totalBytes > PROJECT_WARN_TOTAL_BYTES) {
389
+ } else if (totalBytes > warnProjectBytes) {
379
390
  warnings.push(
380
- `Project files total ${formatBytes(totalBytes)} — approaching the ${Math.floor(
381
- PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024,
382
- )} KB project cap (soft warning from ${Math.floor(
383
- PROJECT_WARN_TOTAL_BYTES / 1024,
384
- )} KB). Keep files lean where structure allows.`,
391
+ `Project files total ${formatBytes(totalBytes)} — approaching this project's ${formatBytes(maxProjectBytes)} cap. Keep files lean where structure allows.`,
385
392
  );
386
393
  }
387
394
  return { errors, warnings };
388
395
  }
389
396
 
390
- export function assertProjectFilesWithinLimits(files) {
391
- const { errors, warnings } = collectProjectLimitFindings(files);
397
+ export function assertProjectFilesWithinLimits(files, projectLimits = {}) {
398
+ const { errors, warnings } = collectProjectLimitFindings(
399
+ files,
400
+ projectLimits,
401
+ );
392
402
  for (const warning of warnings) {
393
403
  console.error(`lumine: warning — ${warning}`);
394
404
  }
@@ -587,6 +597,10 @@ export async function writeProjectMetadata({
587
597
  build.canPublish !== undefined
588
598
  ? Boolean(build.canPublish)
589
599
  : build.role === "owner",
600
+ projectLimits:
601
+ build.projectLimits && typeof build.projectLimits === "object"
602
+ ? build.projectLimits
603
+ : null,
590
604
  }
591
605
  : null,
592
606
  apiUrl: options.apiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.37",
3
+ "version": "0.2.38",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {