@neocompose/cli 0.21.1 → 0.21.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.3] - 2026-08-03
4
+
5
+ ### Fixed
6
+
7
+ - `neo push` paints its preparation spinner before workspace discovery,
8
+ lowering, and NeoScript compilation begin, so large projects no longer look
9
+ stalled before the push plan appears.
10
+ - Animation segment frame values are emitted through the required `value`
11
+ constructor argument. Required sprites use the canonical `SpriteInfo.Empty`
12
+ value; `null` is reserved for nullable `SpriteInfo?` values.
13
+
14
+ ## [0.21.2] - 2026-08-03
15
+
16
+ ### Fixed
17
+
18
+ - Canonical pull emits `@id` only for structurally ambiguous positions such
19
+ as list entries. Class fields, dictionary entries, and constructor arguments
20
+ recover their value rows from the owning value and role instead of exposing
21
+ redundant P61 creation or localization-link identities.
22
+ - Long and nested constructor calls emitted by `neo pull --reset` wrap at a
23
+ stable 88-column default with one named argument per indented line.
24
+
3
25
  ## [0.21.1] - 2026-08-03
4
26
 
5
27
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -842,7 +842,7 @@ function vectorTypes() {
842
842
  members: structuredLeafFieldProperties(name)
843
843
  }));
844
844
  }
845
- var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
845
+ var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, SPRITE_INFO_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID, NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
846
846
  var init_language_spec = __esm({
847
847
  "../packages/neoscript-language/src/language-spec.ts"() {
848
848
  "use strict";
@@ -1006,6 +1006,10 @@ var init_language_spec = __esm({
1006
1006
  kind: "primitive",
1007
1007
  name: "ImageRef"
1008
1008
  };
1009
+ SPRITE_INFO_TYPE = {
1010
+ kind: "primitive",
1011
+ name: "SpriteInfo"
1012
+ };
1009
1013
  FLOAT_OPTIONAL_TYPE = {
1010
1014
  kind: "primitive",
1011
1015
  name: "float",
@@ -1148,12 +1152,26 @@ var init_language_spec = __esm({
1148
1152
  "Returns whether a dialogue pointer has been visited."
1149
1153
  )
1150
1154
  ];
1155
+ NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID = "builtin:SpriteInfo:property:Empty";
1156
+ NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE = Object.freeze({
1157
+ fileId: "",
1158
+ sliceIndex: 0
1159
+ });
1151
1160
  NEOSCRIPT_BUILTIN_TYPE_SYMBOLS = [
1152
1161
  {
1153
1162
  id: "builtin:SpriteInfo",
1154
1163
  name: "SpriteInfo",
1155
1164
  kind: "builtin",
1156
1165
  members: [
1166
+ {
1167
+ ...property(
1168
+ "SpriteInfo",
1169
+ "Empty",
1170
+ SPRITE_INFO_TYPE,
1171
+ "An authored empty sprite at slice zero."
1172
+ ),
1173
+ static: true
1174
+ },
1157
1175
  // `FileId` and `SliceIndex` come from the field table above — they are
1158
1176
  // the writable, addressable fields, not derived properties (D11).
1159
1177
  ...structuredLeafFieldProperties("SpriteInfo"),
@@ -9739,6 +9757,12 @@ var init_strict_resolver = __esm({
9739
9757
  }
9740
9758
  resolveMemberNatural(receiverAst, name, scope, pos, optional) {
9741
9759
  if (receiverAst.kind === "ident" && !scope.lookup(receiverAst.name)) {
9760
+ if (receiverAst.name === "SpriteInfo" && name === "Empty") {
9761
+ return literal(
9762
+ { kind: "primitive", name: "SpriteInfo" },
9763
+ { ...NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE }
9764
+ );
9765
+ }
9742
9766
  const staticType = this.project.typeByName.get(receiverAst.name);
9743
9767
  if (staticType && staticType.kind !== "enum") {
9744
9768
  if (optional) {
@@ -9769,6 +9793,11 @@ var init_strict_resolver = __esm({
9769
9793
  pos
9770
9794
  );
9771
9795
  }
9796
+ if (member.id === NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID) {
9797
+ return literal(member.type, {
9798
+ ...NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE
9799
+ });
9800
+ }
9772
9801
  if (member.computed === true) {
9773
9802
  return {
9774
9803
  pointer: {
@@ -38004,10 +38033,10 @@ function isSpriteValue(value) {
38004
38033
  if (!Number.isInteger(sliceIndex)) return false;
38005
38034
  return sliceIndex >= 0;
38006
38035
  }
38007
- function pendingSpriteValue() {
38036
+ function emptySpriteValue() {
38008
38037
  return { fileId: "", sliceIndex: 0 };
38009
38038
  }
38010
- function isPendingSpriteValue(value) {
38039
+ function isEmptySpriteValue(value) {
38011
38040
  if (!value || typeof value !== "object") return false;
38012
38041
  return getField(value, "fileId") === "" && getField(value, "sliceIndex") === 0;
38013
38042
  }
@@ -38910,6 +38939,7 @@ var init_world_system_classes_generated = __esm({
38910
38939
  {
38911
38940
  memberId: "system_cbd6db9a-f473-44b5-b913-7cdc06452f35",
38912
38941
  memberKind: "sprite",
38942
+ defaultValue: { fileId: "", sliceIndex: 0 },
38913
38943
  required: true,
38914
38944
  schemaKey: "Sprite"
38915
38945
  },
@@ -39032,6 +39062,7 @@ var init_world_system_classes_generated = __esm({
39032
39062
  {
39033
39063
  memberId: "system_e9288ba9-f5a2-4485-8443-6afb155b31e0",
39034
39064
  memberKind: "sprite",
39065
+ defaultValue: { fileId: "", sliceIndex: 0 },
39035
39066
  required: true,
39036
39067
  schemaKey: "Sprite"
39037
39068
  },
@@ -39516,6 +39547,12 @@ var init_world_system_classes_generated = __esm({
39516
39547
  docsText: "One value at a frame index inside a segment. It holds until the next row\nyou author, or until the segment's Duration runs out.",
39517
39548
  extendsWorldKind: "animationFrameBase",
39518
39549
  isAbstract: false,
39550
+ constructorProjections: [
39551
+ {
39552
+ memberId: "system_b71d1f73-da72-49de-ab9a-911cc9bffa43",
39553
+ parameterName: "value"
39554
+ }
39555
+ ],
39519
39556
  genericParams: [
39520
39557
  { id: "system_887b11d3-e56a-47ff-8d01-e2fcefb846a1", name: "T" }
39521
39558
  ],
@@ -42586,7 +42623,7 @@ function stampGenericBindings(value, member, args) {
42586
42623
  }
42587
42624
  function shouldSkipRequiredChildDefault(member) {
42588
42625
  if (member.defaultValue !== void 0) return false;
42589
- return isMemberSpriteBase(member) || isMemberAudioBase(member);
42626
+ return isMemberAudioBase(member);
42590
42627
  }
42591
42628
  function shouldMaterializeChildDefault(member) {
42592
42629
  if (isMemberClassBase(member) && member.partial === true) return false;
@@ -42658,7 +42695,7 @@ function primitiveFallbackValue(document, member) {
42658
42695
  if (isMemberLookupBase(member)) return { value: [] };
42659
42696
  if (isMemberDialogueLookupBase(member)) return { value: [] };
42660
42697
  if (isMemberSpriteBase(member)) {
42661
- return { value: pendingSpriteValue() };
42698
+ return { value: emptySpriteValue() };
42662
42699
  }
42663
42700
  if (isMemberAudioBase(member)) {
42664
42701
  return { value: pendingAudioValue() };
@@ -42682,7 +42719,7 @@ function primitiveFallbackValue(document, member) {
42682
42719
  function isPendingFileMemberValue(member, body) {
42683
42720
  const value = body.value;
42684
42721
  if (isMemberSpriteBase(member)) {
42685
- return isPendingSpriteValue(value);
42722
+ return isEmptySpriteValue(value);
42686
42723
  }
42687
42724
  if (isMemberAudioBase(member)) {
42688
42725
  return isPendingAudioValue(value);
@@ -42768,11 +42805,18 @@ function validateDecimalMemberValue(member, body) {
42768
42805
  }
42769
42806
  function validateSpriteMemberValue(document, member, body) {
42770
42807
  const spriteValue = body.value;
42808
+ if (spriteValue === null) {
42809
+ if (!member.required) return;
42810
+ throw new Error(
42811
+ `Sprite member "${member.name}" is required and cannot be null.`
42812
+ );
42813
+ }
42771
42814
  if (!isSpriteValue(spriteValue)) {
42772
42815
  throw new Error(
42773
42816
  "Sprite value must be { fileId: string, sliceIndex: integer >= 0 }."
42774
42817
  );
42775
42818
  }
42819
+ if (isEmptySpriteValue(spriteValue)) return;
42776
42820
  validateSpriteFileRegistration(document, member, spriteValue.fileId);
42777
42821
  }
42778
42822
  function validateSpriteFileRegistration(document, member, fileId) {
@@ -44993,6 +45037,9 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
44993
45037
  }
44994
45038
  switch (expression.kind) {
44995
45039
  case "litNull":
45040
+ if (!expected.nullable) {
45041
+ throw new Error(`Required value ${path} cannot be null.`);
45042
+ }
44996
45043
  return null;
44997
45044
  case "litBool":
44998
45045
  case "litString":
@@ -45212,6 +45259,9 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
45212
45259
  );
45213
45260
  }
45214
45261
  case "member": {
45262
+ if (expected.name === "SpriteInfo" && expression.name === "Empty" && expression.receiver.kind === "ident" && expression.receiver.name === "SpriteInfo") {
45263
+ return { fileId: "", sliceIndex: 0 };
45264
+ }
45215
45265
  const fileId = projectFileIdForPath(context, expression);
45216
45266
  if (fileId !== null) return { fileId };
45217
45267
  if (expression.name === "Slice" || expression.receiver.kind === "call") {
@@ -75392,7 +75442,6 @@ function buildValueEmitContext(records2, manifest) {
75392
75442
  )
75393
75443
  ),
75394
75444
  fileSymbols: projectFileSymbols(records2),
75395
- referencedValueIds: externallyReferencedValueIds(records2),
75396
75445
  localizedTextIds: /* @__PURE__ */ new Set(),
75397
75446
  constructionReplays: /* @__PURE__ */ new Map()
75398
75447
  };
@@ -77144,7 +77193,12 @@ function lowerValueRow(context, member, sourceExpression, expectedValueId, sourc
77144
77193
  }
77145
77194
  function lowerValueBody(context, member, expression, base, source, environment, authoredSlice) {
77146
77195
  const next = valueFileFields(base);
77147
- if (expression.kind === "litNull") return { ...next, value: null };
77196
+ if (expression.kind === "litNull") {
77197
+ if (member.required) {
77198
+ throw new Error(`Required member ${member.name} cannot store null.`);
77199
+ }
77200
+ return { ...next, value: null };
77201
+ }
77148
77202
  switch (member.kind) {
77149
77203
  case "null":
77150
77204
  return { ...next, value: null };
@@ -78128,6 +78182,9 @@ function expressionAnchorPos(expression) {
78128
78182
  }
78129
78183
  function lowerFileValue(context, member, expression) {
78130
78184
  if (member.kind === "sprite") {
78185
+ if (isSpriteInfoEmptyExpression(expression)) {
78186
+ return { fileId: "", sliceIndex: 0 };
78187
+ }
78131
78188
  if (expression.kind !== "call" || expression.callee.kind !== "member" || expression.callee.name !== "Slice") {
78132
78189
  throw new Error(
78133
78190
  `Sprite member ${member.name} requires Image.Slice(index).`
@@ -79115,7 +79172,8 @@ function emitValueBody(context, member, value, visited, targetTyped, environment
79115
79172
  context,
79116
79173
  kind,
79117
79174
  `Audio member ${JSON.stringify(String(member.name ?? member.id))}`,
79118
- body
79175
+ body,
79176
+ member.required === true
79119
79177
  );
79120
79178
  }
79121
79179
  if (kind === 5)
@@ -79235,7 +79293,10 @@ function classValue(context, member, value, visited, targetTyped, outerEnvironme
79235
79293
  `${key} = ${emitValue(context, emittedChildMember, childId, {
79236
79294
  environment,
79237
79295
  definitionSite: !context.symbolsByValueId.has(childId),
79238
- writeIdentity: !context.symbolsByValueId.has(childId) && context.referencedValueIds.has(childId),
79296
+ // A class slot recovers this row from the owning value and schema key.
79297
+ // P61 creation metadata and localization back-pointers must not turn
79298
+ // that structural child into an explicitly identified source value.
79299
+ writeIdentity: false,
79239
79300
  visited
79240
79301
  })}`
79241
79302
  );
@@ -79245,7 +79306,7 @@ function classValue(context, member, value, visited, targetTyped, outerEnvironme
79245
79306
  ${fields.map((field) => indentNeoSourceNonEmptyLines(field, 2)).join(",\n")}
79246
79307
  }`;
79247
79308
  }
79248
- const constructor2 = storedConstruction !== null ? storedConstruction : projection.arguments.length > 0 ? `new ${name}(${projection.arguments.join(", ")})` : targetTyped ? "new()" : `new ${name}()`;
79309
+ const constructor2 = storedConstruction !== null ? storedConstruction : projection.arguments.length > 0 ? constructorCallSource(`new ${name}`, projection.arguments) : targetTyped ? "new()" : `new ${name}()`;
79249
79310
  return fields.length === 0 ? constructor2 : `${storedConstruction !== null || projection.arguments.length > 0 ? constructor2 : targetTyped ? "new()" : `new ${name}`} {
79250
79311
  ${fields.map((field) => indentNeoSourceNonEmptyLines(field, 2)).join(",\n")}
79251
79312
  }`;
@@ -79322,7 +79383,16 @@ function storedConstructorCallSource(context, schemaClass2, value, className, en
79322
79383
  cloneAggregateArguments
79323
79384
  )}`;
79324
79385
  });
79325
- return argumentsValue.length === 0 && targetTyped ? "new()" : `new ${className}(${argumentsValue.join(", ")})`;
79386
+ return argumentsValue.length === 0 && targetTyped ? "new()" : constructorCallSource(`new ${className}`, argumentsValue);
79387
+ }
79388
+ function constructorCallSource(callee, argumentsValue) {
79389
+ const inline = `${callee}(${argumentsValue.join(", ")})`;
79390
+ if (!argumentsValue.some((argument2) => argument2.includes("\n")) && inline.length <= CANONICAL_CONSTRUCTOR_INLINE_WIDTH) {
79391
+ return inline;
79392
+ }
79393
+ return `${callee}(
79394
+ ${argumentsValue.map((argument2) => `${indentNeoSourceNonEmptyLines(argument2, 2)},`).join("\n")}
79395
+ )`;
79326
79396
  }
79327
79397
  function storedValueConstructor(context, schemaClass2, constructorArgs) {
79328
79398
  const requiredId = schemaClass2.requiredConstructorId;
@@ -79353,7 +79423,12 @@ function storedValueConstructor(context, schemaClass2, constructorArgs) {
79353
79423
  return selected2;
79354
79424
  }
79355
79425
  function storedConstructorArgumentSource(context, type, value, environment, visited, cloneAggregateArguments = false) {
79356
- if (value === null) return "null";
79426
+ if (value === null) {
79427
+ if (!type.nullable) {
79428
+ throw new Error("Required constructor argument stores null.");
79429
+ }
79430
+ return "null";
79431
+ }
79357
79432
  switch (type.kind) {
79358
79433
  case "null":
79359
79434
  return "null";
@@ -79385,7 +79460,8 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79385
79460
  {
79386
79461
  id: "__constructor_arg__",
79387
79462
  name: "argument",
79388
- kind: 11 /* Sprite */
79463
+ kind: 11 /* Sprite */,
79464
+ required: !type.nullable
79389
79465
  },
79390
79466
  value
79391
79467
  );
@@ -79394,7 +79470,8 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79394
79470
  context,
79395
79471
  12 /* Audio */,
79396
79472
  "constructor argument",
79397
- value
79473
+ value,
79474
+ !type.nullable
79398
79475
  );
79399
79476
  case "enum":
79400
79477
  return enumValue(
@@ -79419,7 +79496,14 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79419
79496
  if (cloneAggregateArguments) {
79420
79497
  return storedAggregateCloneSource(context, type, value, environment);
79421
79498
  }
79422
- return storedTypedRowSource(context, type, value, environment, visited);
79499
+ return storedTypedRowSource(
79500
+ context,
79501
+ type,
79502
+ value,
79503
+ environment,
79504
+ visited,
79505
+ false
79506
+ );
79423
79507
  }
79424
79508
  case "interface":
79425
79509
  case "generic":
@@ -79433,7 +79517,14 @@ function storedConstructorArgumentSource(context, type, value, environment, visi
79433
79517
  if (cloneAggregateArguments) {
79434
79518
  return storedAggregateCloneSource(context, type, value, environment);
79435
79519
  }
79436
- return storedTypedRowSource(context, type, value, environment, visited);
79520
+ return storedTypedRowSource(
79521
+ context,
79522
+ type,
79523
+ value,
79524
+ environment,
79525
+ visited,
79526
+ false
79527
+ );
79437
79528
  case "lookup":
79438
79529
  return referenceValue(
79439
79530
  context,
@@ -79552,7 +79643,7 @@ function storedSchemaTypeName(context, type, environment, valueId) {
79552
79643
  return "null";
79553
79644
  }
79554
79645
  }
79555
- function storedTypedRowSource(context, type, valueId, environment, visited) {
79646
+ function storedTypedRowSource(context, type, valueId, environment, visited, writeIdentity) {
79556
79647
  const symbol = context.symbolsByValueId.get(valueId);
79557
79648
  if (symbol !== void 0) return symbol;
79558
79649
  const row = context.values.get(valueId);
@@ -79575,32 +79666,34 @@ function storedTypedRowSource(context, type, valueId, environment, visited) {
79575
79666
  `Stored ${type.kind} constructor argument ${valueId} has no concrete class.`
79576
79667
  );
79577
79668
  }
79578
- return `@id(${quote4(valueId)})
79579
- ${classValue(
79580
- context,
79581
- {
79582
- id: `__constructor_arg__:${valueId}`,
79583
- name: "argument",
79584
- kind: 7 /* Class */,
79585
- classId,
79586
- classArguments: {},
79587
- partial: false
79588
- },
79589
- row,
79590
- visited,
79591
- false,
79592
- environment
79593
- )}`;
79669
+ return storedRowIdentitySource(
79670
+ valueId,
79671
+ writeIdentity,
79672
+ classValue(
79673
+ context,
79674
+ {
79675
+ id: `__constructor_arg__:${valueId}`,
79676
+ name: "argument",
79677
+ kind: 7 /* Class */,
79678
+ classId,
79679
+ classArguments: {},
79680
+ partial: false
79681
+ },
79682
+ row,
79683
+ visited,
79684
+ false,
79685
+ environment
79686
+ )
79687
+ );
79594
79688
  }
79595
79689
  if (type.kind === "list") {
79596
- if (row.value === null) return "null";
79690
+ if (row.value === null) {
79691
+ return storedRowIdentitySource(valueId, writeIdentity, "null");
79692
+ }
79597
79693
  const ids = Array.isArray(row.value) ? row.value.filter(
79598
79694
  (entry) => typeof entry === "string"
79599
79695
  ) : [...context.values].filter(([, candidate]) => candidate.containerId === valueId).map(([id2]) => id2).sort(compareCodePoints);
79600
- if (ids.length === 0) return `@id(${quote4(valueId)})
79601
- []`;
79602
- return `@id(${quote4(valueId)})
79603
- [
79696
+ const expression = ids.length === 0 ? "[]" : `[
79604
79697
  ${ids.map(
79605
79698
  (id2) => indentNeoSourceNonEmptyLines(
79606
79699
  storedTypedRowSource(
@@ -79608,22 +79701,26 @@ ${ids.map(
79608
79701
  type.entryType,
79609
79702
  id2,
79610
79703
  environment,
79611
- visited
79704
+ visited,
79705
+ true
79612
79706
  ),
79613
79707
  2
79614
79708
  )
79615
79709
  ).join(",\n\n")}
79616
79710
  ]`;
79711
+ return storedRowIdentitySource(valueId, writeIdentity, expression);
79617
79712
  }
79618
79713
  if (type.kind === "dictionary") {
79619
- if (row.value === null) return "null";
79620
- if (!isObjectRecord2(row.value)) return "{}";
79714
+ if (row.value === null) {
79715
+ return storedRowIdentitySource(valueId, writeIdentity, "null");
79716
+ }
79717
+ if (!isObjectRecord2(row.value)) {
79718
+ return storedRowIdentitySource(valueId, writeIdentity, "{}");
79719
+ }
79621
79720
  const entries = Object.entries(row.value).sort(
79622
79721
  ([left], [right]) => compareCodePoints(left, right)
79623
79722
  );
79624
- return entries.length === 0 ? `@id(${quote4(valueId)})
79625
- {}` : `@id(${quote4(valueId)})
79626
- {
79723
+ const expression = entries.length === 0 ? "{}" : `{
79627
79724
  ${entries.flatMap(
79628
79725
  ([key, id2]) => typeof id2 === "string" ? [
79629
79726
  indentNeoSourceNonEmptyLines(
@@ -79632,53 +79729,71 @@ ${entries.flatMap(
79632
79729
  type.entryType,
79633
79730
  id2,
79634
79731
  environment,
79635
- visited
79732
+ visited,
79733
+ false
79636
79734
  )}`,
79637
79735
  2
79638
79736
  )
79639
79737
  ] : []
79640
79738
  ).join(",\n")}
79641
79739
  }`;
79740
+ return storedRowIdentitySource(valueId, writeIdentity, expression);
79642
79741
  }
79643
79742
  if (type.kind === "lookup") {
79644
- return referenceValue(
79645
- context,
79646
- {
79647
- id: `__constructor_arg__:${valueId}`,
79648
- name: "argument",
79649
- kind: 9 /* Lookup */,
79650
- collectionMemberId: type.collectionMemberId,
79651
- collectionValueId: type.collectionValueId,
79652
- multiselect: false
79653
- },
79654
- row.value,
79655
- false
79743
+ return storedRowIdentitySource(
79744
+ valueId,
79745
+ writeIdentity,
79746
+ referenceValue(
79747
+ context,
79748
+ {
79749
+ id: `__constructor_arg__:${valueId}`,
79750
+ name: "argument",
79751
+ kind: 9 /* Lookup */,
79752
+ collectionMemberId: type.collectionMemberId,
79753
+ collectionValueId: type.collectionValueId,
79754
+ multiselect: false
79755
+ },
79756
+ row.value,
79757
+ false
79758
+ )
79656
79759
  );
79657
79760
  }
79658
79761
  if (type.kind === "dialogueLookup") {
79659
- return referenceValue(
79660
- context,
79661
- {
79662
- id: `__constructor_arg__:${valueId}`,
79663
- name: "argument",
79664
- kind: 18 /* DialogueLookup */,
79665
- multiselect: false
79666
- },
79667
- row.value,
79668
- true
79762
+ return storedRowIdentitySource(
79763
+ valueId,
79764
+ writeIdentity,
79765
+ referenceValue(
79766
+ context,
79767
+ {
79768
+ id: `__constructor_arg__:${valueId}`,
79769
+ name: "argument",
79770
+ kind: 18 /* DialogueLookup */,
79771
+ multiselect: false
79772
+ },
79773
+ row.value,
79774
+ true
79775
+ )
79669
79776
  );
79670
79777
  }
79671
- return storedConstructorArgumentSource(
79672
- context,
79673
- type,
79674
- row.value,
79675
- environment,
79676
- visited
79778
+ return storedRowIdentitySource(
79779
+ valueId,
79780
+ writeIdentity,
79781
+ storedConstructorArgumentSource(
79782
+ context,
79783
+ type,
79784
+ row.value,
79785
+ environment,
79786
+ visited
79787
+ )
79677
79788
  );
79678
79789
  } finally {
79679
79790
  visited.delete(valueId);
79680
79791
  }
79681
79792
  }
79793
+ function storedRowIdentitySource(valueId, writeIdentity, expression) {
79794
+ return `${writeIdentity ? `@id(${quote4(valueId)})
79795
+ ` : ""}${expression}`;
79796
+ }
79682
79797
  function storedVectorSource(value, name, keys) {
79683
79798
  if (!isObjectRecord2(value)) {
79684
79799
  throw new Error(`Stored ${name} constructor argument is not an object.`);
@@ -80001,7 +80116,9 @@ ${entries.flatMap(
80001
80116
  `${quote4(key)}: ${emitValue(context, entryMember, id2, {
80002
80117
  environment,
80003
80118
  definitionSite: !context.symbolsByValueId.has(id2),
80004
- writeIdentity: !context.symbolsByValueId.has(id2) && context.referencedValueIds.has(id2),
80119
+ // Dictionary keys, like class schema keys, recover entry
80120
+ // identity from the owning row. Only list entries need an id.
80121
+ writeIdentity: false,
80005
80122
  visited
80006
80123
  })}`,
80007
80124
  2
@@ -80129,8 +80246,13 @@ function delegateValue(context, body) {
80129
80246
  }
80130
80247
  return target.name;
80131
80248
  }
80132
- function fileValue(context, kind, label, body) {
80133
- if (body === null || body === void 0) return "null";
80249
+ function fileValue(context, kind, label, body, required2) {
80250
+ if (body === null || body === void 0) {
80251
+ if (required2) {
80252
+ throw new Error(`${label} is required and cannot store null.`);
80253
+ }
80254
+ return "null";
80255
+ }
80134
80256
  if (!isObjectRecord2(body)) {
80135
80257
  throw new Error(
80136
80258
  `${label} stores ${JSON.stringify(body)}, which is not a file value record.`
@@ -80142,12 +80264,18 @@ function fileValue(context, kind, label, body) {
80142
80264
  );
80143
80265
  }
80144
80266
  if (body.fileId.length === 0) {
80145
- throw new Error(`${label} stores an empty fileId.`);
80267
+ if (kind === 11 && numberOr2(body.sliceIndex, 0) === 0) {
80268
+ return "SpriteInfo.Empty";
80269
+ }
80270
+ throw new Error(`${label} stores an invalid empty file value.`);
80146
80271
  }
80147
80272
  const base = fileRegistrySource(context, kind, body.fileId);
80148
80273
  if (kind !== 11) return base;
80149
80274
  return `${base}.Slice(${numberOr2(body.sliceIndex, 0)})`;
80150
80275
  }
80276
+ function isSpriteInfoEmptyExpression(expression) {
80277
+ return expression.kind === "member" && expression.name === "Empty" && expression.receiver.kind === "ident" && expression.receiver.name === "SpriteInfo";
80278
+ }
80151
80279
  function fileRegistrySource(context, kind, fileId) {
80152
80280
  const symbol = context.fileSymbols.get(fileId);
80153
80281
  if (symbol !== void 0) {
@@ -80172,7 +80300,9 @@ function structuredLeafSource(context, kind, member, body) {
80172
80300
  ${assignments.map((entry) => indentNeoSourceNonEmptyLines(entry, 2)).join(",\n")}
80173
80301
  }`;
80174
80302
  }
80175
- if (kind === 11 /* Sprite */) return fileValue(context, 11, label, body);
80303
+ if (kind === 11 /* Sprite */) {
80304
+ return fileValue(context, 11, label, body, member.required === true);
80305
+ }
80176
80306
  if (body === null) return "null";
80177
80307
  if (!isObjectRecord2(body)) return "new()";
80178
80308
  return `new(${structuredLeafFieldKeys(kind).map((key) => scalar(body[key])).join(", ")})`;
@@ -80215,33 +80345,6 @@ function projectFileSymbols(records2) {
80215
80345
  });
80216
80346
  return assignDeterministicFileSymbols(files);
80217
80347
  }
80218
- function externallyReferencedValueIds(records2) {
80219
- const counts = /* @__PURE__ */ new Map();
80220
- const visit = (value) => {
80221
- if (typeof value === "string") {
80222
- if (records2.has(`value:${value}`))
80223
- counts.set(value, (counts.get(value) ?? 0) + 1);
80224
- return;
80225
- }
80226
- if (Array.isArray(value)) {
80227
- value.forEach((entry) => visit(entry));
80228
- return;
80229
- }
80230
- if (isObjectRecord2(value)) {
80231
- for (const [childKey, child] of Object.entries(value)) {
80232
- if (childKey === "id") continue;
80233
- if (childKey === "sourceValueId") continue;
80234
- visit(child);
80235
- }
80236
- }
80237
- };
80238
- for (const record3 of records2.values()) {
80239
- if (!record3.deleted) visit(record3.data);
80240
- }
80241
- return new Set(
80242
- [...counts].filter(([, count]) => count > 1).map(([id2]) => id2)
80243
- );
80244
- }
80245
80348
  function projectMainLocale(records2) {
80246
80349
  const config = [...records2.values()].find(
80247
80350
  (record3) => !record3.deleted && record3.recordKind === "localization-config"
@@ -80330,7 +80433,7 @@ function memberDefaultBody(member) {
80330
80433
  if (!("value" in defaultValue)) return null;
80331
80434
  return defaultValue.value;
80332
80435
  }
80333
- var INFERRED_GENERIC_CLASS_PREFIX, MEMBER_KIND_DICTIONARY, MEMBER_KIND_LIST, MEMBER_KIND_CLASS;
80436
+ var INFERRED_GENERIC_CLASS_PREFIX, MEMBER_KIND_DICTIONARY, MEMBER_KIND_LIST, MEMBER_KIND_CLASS, CANONICAL_CONSTRUCTOR_INLINE_WIDTH;
80334
80437
  var init_value_sources = __esm({
80335
80438
  "src/project-source/value-sources.ts"() {
80336
80439
  "use strict";
@@ -80356,6 +80459,7 @@ var init_value_sources = __esm({
80356
80459
  MEMBER_KIND_DICTIONARY = 5;
80357
80460
  MEMBER_KIND_LIST = 6;
80358
80461
  MEMBER_KIND_CLASS = 7;
80462
+ CANONICAL_CONSTRUCTOR_INLINE_WIDTH = 88;
80359
80463
  }
80360
80464
  });
80361
80465
 
@@ -94833,7 +94937,7 @@ async function downloadProjectVersionTransactionResult(args) {
94833
94937
  cursor = page.continueCursor;
94834
94938
  }
94835
94939
  }
94836
- async function runPush(workspace, options) {
94940
+ async function preparePushStatus(workspace, options) {
94837
94941
  const status = computeWorkspaceStatus(workspace);
94838
94942
  if (status.conflictedFiles.length > 0) {
94839
94943
  throw new Error(
@@ -94862,22 +94966,7 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
94862
94966
  changes: status.changes,
94863
94967
  binaryChanges: status.binaryChanges ?? []
94864
94968
  });
94865
- if (status.changes.length === 0) {
94866
- if (options.json === true) {
94867
- console.log(
94868
- JSON.stringify({
94869
- type: "push-plan",
94870
- dryRun: options.dryRun,
94871
- changeCount: 0,
94872
- staticValueSeedCount: 0,
94873
- changes: []
94874
- })
94875
- );
94876
- } else {
94877
- console.log("Nothing to push \u2014 working copy is clean.");
94878
- }
94879
- return;
94880
- }
94969
+ if (status.changes.length === 0) return status;
94881
94970
  const initConversions = status.initConversions ?? [];
94882
94971
  if (initConversions.length > 0 && options.json !== true) {
94883
94972
  for (const line of describeInitConversions(initConversions)) warn(line);
@@ -94949,8 +95038,40 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
94949
95038
  } catch (error) {
94950
95039
  reportDryRunFailure(error, options.json === true);
94951
95040
  process.exitCode = 1;
94952
- return;
95041
+ return null;
95042
+ }
95043
+ }
95044
+ return status;
95045
+ }
95046
+ async function runPush(workspace, options, preparationOverride) {
95047
+ const preparation = preparationOverride === void 0 ? process.stdout.isTTY === true && options.json !== true ? spinner("Preparing push\u2026") : null : preparationOverride;
95048
+ let status;
95049
+ try {
95050
+ status = await preparePushStatus(workspace, options);
95051
+ } catch (error) {
95052
+ preparation?.fail("Push preparation failed.");
95053
+ throw error;
95054
+ }
95055
+ if (status === null) {
95056
+ preparation?.fail("Push preparation failed.");
95057
+ return;
95058
+ }
95059
+ preparation?.stop();
95060
+ if (status.changes.length === 0) {
95061
+ if (options.json === true) {
95062
+ console.log(
95063
+ JSON.stringify({
95064
+ type: "push-plan",
95065
+ dryRun: options.dryRun,
95066
+ changeCount: 0,
95067
+ staticValueSeedCount: 0,
95068
+ changes: []
95069
+ })
95070
+ );
95071
+ } else {
95072
+ console.log("Nothing to push \u2014 working copy is clean.");
94953
95073
  }
95074
+ return;
94954
95075
  }
94955
95076
  if (options.json === true) {
94956
95077
  console.log(
@@ -98075,14 +98196,29 @@ async function main() {
98075
98196
  return;
98076
98197
  }
98077
98198
  case "push": {
98078
- const workspace = loadWorkspaceForCommand(args);
98079
- const { runPush: runPush2 } = await Promise.resolve().then(() => (init_push(), push_exports));
98080
- await runPush2(workspace, {
98081
- dryRun: boolFlag(args, "dry-run"),
98082
- summary: stringFlag(args, "summary"),
98083
- acceptBump: boolFlag(args, "accept-bump"),
98084
- json: boolFlag(args, "json")
98085
- });
98199
+ const json = boolFlag(args, "json");
98200
+ const preparation = process.stdout.isTTY === true && json !== true ? spinner("Preparing push\u2026") : null;
98201
+ let handedOffPreparation = false;
98202
+ try {
98203
+ const workspace = loadWorkspaceForCommand(args);
98204
+ const { runPush: runPush2 } = await Promise.resolve().then(() => (init_push(), push_exports));
98205
+ handedOffPreparation = true;
98206
+ await runPush2(
98207
+ workspace,
98208
+ {
98209
+ dryRun: boolFlag(args, "dry-run"),
98210
+ summary: stringFlag(args, "summary"),
98211
+ acceptBump: boolFlag(args, "accept-bump"),
98212
+ json
98213
+ },
98214
+ preparation
98215
+ );
98216
+ } catch (error) {
98217
+ if (!handedOffPreparation) {
98218
+ preparation?.fail("Push preparation failed.");
98219
+ }
98220
+ throw error;
98221
+ }
98086
98222
  return;
98087
98223
  }
98088
98224
  case "status": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.21.1",
3
+ "version": "0.21.3",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.21.1 -->
12
+ <!-- reviewed-through-cli: 0.21.3 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -66,12 +66,15 @@ Use complete values in normal member initialization:
66
66
 
67
67
  ```neo
68
68
  Sprite = Images.Hero.Slice(0);
69
+ Placeholder = SpriteInfo.Empty;
69
70
  Position = new(0, 0, 0);
70
71
  Tint = Color(1, 1, 1, 1);
71
72
  ```
72
73
 
73
74
  A bare `Images.Hero` yields a file ID only in a file-valued field context.
74
75
  Use `.Slice(index)` for a complete `SpriteInfo`.
76
+ Use `SpriteInfo.Empty` for an authored non-null empty sprite. Use `null` only
77
+ when the declared type is nullable `SpriteInfo?`.
75
78
 
76
79
  NeoScript may read or write structured fields when the receiver's effective
77
80
  storage permits it:
@@ -73,7 +73,7 @@ wrappers.
73
73
  The marker near the top of `SKILL.md` must exactly match the package version:
74
74
 
75
75
  ```html
76
- <!-- reviewed-through-cli: 0.21.1 -->
76
+ <!-- reviewed-through-cli: 0.21.3 -->
77
77
  ```
78
78
 
79
79
  The quoted version above is checked too, so this instruction cannot go stale
@@ -37,6 +37,10 @@ validation — so a rejection that used to cost a real push and return an opaque
37
37
  `preparation-failed` now reports before anything is sent. It does not prompt,
38
38
  upload, commit, or rewrite source.
39
39
 
40
+ Interactive pushes show `Preparing push…` before workspace discovery,
41
+ lowering, and compilation begin. JSON and non-TTY output remain structured and
42
+ do not emit that terminal-only spinner.
43
+
40
44
  A clean dry-run does not promise a clean push. The workspace holds its
41
45
  last-pulled state, so everything that grades the live head is still server-only:
42
46
  re-verification of the uploaded bundle bytes, project-file staging, the