@neocompose/cli 0.69.0 → 0.69.1

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.69.1] - 2026-10-06
4
+
5
+ - `neo export` shows a spinner while it works, labeled with the current step: reading the pulled project, building the export, generating Unity types, writing changed files, and checking local changes.
6
+ - `neo push` no longer carries a replaced class member's stored value into a new member with the same name. The old member's value rows are deleted and the new member's value is seeded fresh. Before this fix, push could fail with errors such as "single-select lookup cannot store multiple targets".
7
+ - `neo push --dry-run` can replay a stored construction that sets a `NeoAction` member, for example `OnApply = []` on a watcher stored inside an item stack. Before this fix, the dry run's round-trip check failed with "Cannot infer the entry type of an empty list".
8
+
3
9
  ## [0.69.0] - 2026-10-05
4
10
 
5
11
  - **Breaking:** `neo export` replaces `neo export unity --out <dir>`. It builds the Unity export from the last pull, offline, and writes it into the directories the Unity project's `NeoComposeConfig` asset names. It writes only files whose bytes changed, skips codegen when only values changed since the last export into that Unity project, and writes `Library/NeoCompose/export.json` last for Unity to ingest. Local edits aren't exported, and the output counts them. An error diagnostic, or a project file whose bytes differ from the pulled file, stops the export before anything is written.
package/dist/neo.mjs CHANGED
@@ -17687,6 +17687,17 @@ var init_strict_resolver = __esm({
17687
17687
  targetType,
17688
17688
  schemaClass2
17689
17689
  );
17690
+ if (memberType2.kind === "action") {
17691
+ return {
17692
+ schemaKey: member.schemaKey ?? member.name,
17693
+ memberId: member.id,
17694
+ valuePointer: this.resolveReplayedListenerSet(
17695
+ entry.value,
17696
+ memberType2,
17697
+ scope
17698
+ )
17699
+ };
17700
+ }
17690
17701
  const value = this.resolveExpression(entry.value, scope, memberType2);
17691
17702
  if (!isNeoScriptTypeAssignable(value.type, memberType2, this.project)) {
17692
17703
  throw new CompileError(
@@ -17701,6 +17712,43 @@ var init_strict_resolver = __esm({
17701
17712
  };
17702
17713
  });
17703
17714
  }
17715
+ /**
17716
+ * P62 §2. A stored action slot replays as the listener list it stores. Each
17717
+ * entry resolves to the same static (member, row) identity the authored list
17718
+ * lowers to. Executable script cannot reassign a listener set (§3.2).
17719
+ */
17720
+ resolveReplayedListenerSet(expression, actionType, scope) {
17721
+ if (this.context.storedConstructionReplay !== true) {
17722
+ throw new CompileError(
17723
+ `Cannot assign to ${this.describe(actionType)}; a listener set is not reassignable from script. Use '+=' to add a listener or '-=' to remove one.`,
17724
+ expression.pos
17725
+ );
17726
+ }
17727
+ if (expression.kind !== "litList") {
17728
+ throw new CompileError(
17729
+ `A stored ${this.describe(actionType)} replays as a bracketed listener list.`,
17730
+ expression.pos
17731
+ );
17732
+ }
17733
+ const listeners = [];
17734
+ for (const element of expression.elements) {
17735
+ const listener = this.resolveActionListener(element, actionType, scope);
17736
+ if (listener.type !== "value" /* Value */) {
17737
+ throw new CompileError(
17738
+ "A stored listener must name a stable (member, row) target.",
17739
+ element.pos
17740
+ );
17741
+ }
17742
+ listeners.push(listener.value.value);
17743
+ }
17744
+ return {
17745
+ type: "value" /* Value */,
17746
+ value: {
17747
+ typeInfo: toWireType(actionType, this.project),
17748
+ value: { listeners }
17749
+ }
17750
+ };
17751
+ }
17704
17752
  resolveTileConversion(callee, args, typeArguments, scope, pos) {
17705
17753
  const receiver = this.resolveExpression(callee.receiver, scope);
17706
17754
  const owner = receiver.type.kind === "named" ? this.project.typeById.get(receiver.type.typeId) : void 0;
@@ -31763,7 +31811,7 @@ var init_registry = __esm({
31763
31811
  "../packages/manifest/schema-contract/registry.mjs"() {
31764
31812
  "use strict";
31765
31813
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
31766
- cliVersion: "0.69.0",
31814
+ cliVersion: "0.69.1",
31767
31815
  projectFileUploadBatchSize: 32,
31768
31816
  documentRecords: {
31769
31817
  member: {
@@ -90621,7 +90669,6 @@ var project_document_read_exports = {};
90621
90669
  __export(project_document_read_exports, {
90622
90670
  DOCUMENT_MANIFEST_PAGE_SIZE: () => DOCUMENT_MANIFEST_PAGE_SIZE,
90623
90671
  assembleProjectDocumentRaw: () => assembleProjectDocumentRaw,
90624
- createDocumentPageReader: () => createDocumentPageReader,
90625
90672
  fetchProjectDocumentPartitionRecords: () => fetchProjectDocumentPartitionRecords,
90626
90673
  fetchProjectDocumentRawChunked: () => fetchProjectDocumentRawChunked,
90627
90674
  fetchProjectDocumentRawChunkedOnce: () => fetchProjectDocumentRawChunkedOnce,
@@ -130418,14 +130465,15 @@ function retainedStoredClassBody(context, storedClassId, baseBody) {
130418
130465
  const storedMemberIds = storedClassSchemaMemberIds(context, storedClassId);
130419
130466
  const body = {};
130420
130467
  for (const [key, childId] of Object.entries(baseBody)) {
130421
- const storedMemberId = storedMemberIds.get(key);
130422
- if (storedMemberId !== void 0 && !context.members.has(storedMemberId)) {
130423
- continue;
130424
- }
130468
+ if (storedMemberRemoved(context, storedMemberIds, key)) continue;
130425
130469
  body[key] = childId;
130426
130470
  }
130427
130471
  return body;
130428
130472
  }
130473
+ function storedMemberRemoved(context, storedMemberIds, key) {
130474
+ const storedMemberId = storedMemberIds.get(key);
130475
+ return storedMemberId !== void 0 && !context.members.has(storedMemberId);
130476
+ }
130429
130477
  function storedClassSchemaMemberIds(context, classId) {
130430
130478
  const cached = context.storedClassSchemaMemberIds.get(classId);
130431
130479
  if (cached !== void 0) return cached;
@@ -131745,7 +131793,7 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
131745
131793
  }
131746
131794
  for (const assignment of expression.initializer ?? []) {
131747
131795
  const childMember = classMemberByName(context, classId, assignment.name);
131748
- const childId = baseBody[assignment.name];
131796
+ const childId = body[assignment.name];
131749
131797
  if (typeof childId !== "string") {
131750
131798
  const symbolId2 = sourceValueSymbol(context, assignment.value);
131751
131799
  if (symbolId2 !== null) {
@@ -131817,6 +131865,7 @@ function lowerClassValue(context, member, expression, base, source, outerEnviron
131817
131865
  for (const [schemaKey, childId] of Object.entries(baseBody)) {
131818
131866
  if (typeof childId !== "string") continue;
131819
131867
  if (resetSchemaKeys.has(schemaKey)) continue;
131868
+ if (storedMemberRemoved(context, storedMemberIds, schemaKey)) continue;
131820
131869
  if (materializedConstruction === "lower" && authoredKeys.has(schemaKey)) {
131821
131870
  continue;
131822
131871
  }
@@ -179970,14 +180019,28 @@ async function runExport(workspace, options) {
179970
180019
  workspace,
179971
180020
  options.unityProject
179972
180021
  );
179973
- const result = exportUnityProject({
179974
- workspace,
179975
- exportSettings,
179976
- convexUrl,
179977
- target
179978
- });
180022
+ const progress = spinner("Reading pulled project\u2026");
180023
+ let result;
180024
+ let local;
180025
+ try {
180026
+ result = exportUnityProject({
180027
+ workspace,
180028
+ exportSettings,
180029
+ convexUrl,
180030
+ target,
180031
+ onPhase: (label) => progress.update(label)
180032
+ });
180033
+ progress.update("Checking local changes\u2026");
180034
+ local = localChanges(workspace);
180035
+ } catch (error) {
180036
+ progress.fail("Export failed.");
180037
+ throw error;
180038
+ }
179979
180039
  const { phases } = result;
179980
180040
  const elapsed = phases.read + phases.build + phases.codegen + phases.write;
180041
+ progress.succeed(
180042
+ `Exported ${color.bold(exportSettings.semver.label)} ${color.dim(`(${workspace.config.versionId})`)} into ${target.unityProjectRoot} in ${elapsed} ms ${color.dim(`(read ${phases.read}, build ${phases.build}, codegen ${phases.codegen}, write ${phases.write})`)}.`
180043
+ );
179981
180044
  for (const diagnostic of result.diagnostics) {
179982
180045
  if (diagnostic.severity === "error") continue;
179983
180046
  warn(formatDiagnostic(diagnostic));
@@ -179987,16 +180050,12 @@ async function runExport(workspace, options) {
179987
180050
  `Unity's project.json was written at schema ${result.schemaSkew}; this export writes schema ${PROJECT_UNITY_EXPORT_SCHEMA_VERSION}. Update the Neo Compose SDK in Unity to a version that reads schema ${PROJECT_UNITY_EXPORT_SCHEMA_VERSION}.`
179988
180051
  );
179989
180052
  }
179990
- success(
179991
- `Exported ${color.bold(exportSettings.semver.label)} ${color.dim(`(${workspace.config.versionId})`)} into ${target.unityProjectRoot} in ${elapsed} ms ${color.dim(`(read ${phases.read}, build ${phases.build}, codegen ${phases.codegen}, write ${phases.write})`)}.`
179992
- );
179993
180053
  console.log(
179994
180054
  ` ${result.written.length} file(s) written, ${result.deleted.length} deleted, ${result.unchanged} unchanged; codegen ${result.codegenRan ? "ran" : "skipped"}.`
179995
180055
  );
179996
180056
  console.log(
179997
180057
  result.sidecarWritten ? " Unity ingests the export on its next editor tick." : " Unity is current."
179998
180058
  );
179999
- const local = localChanges(workspace);
180000
180059
  if (local.errors > 0) {
180001
180060
  note(
180002
180061
  ` Local sources have ${local.errors} error(s) and are not in the export, which holds the pulled head. Run \`neo status\` to see them.`
@@ -180008,7 +180067,7 @@ async function runExport(workspace, options) {
180008
180067
  }
180009
180068
  }
180010
180069
  function exportUnityProject(args) {
180011
- const { workspace, exportSettings, target } = args;
180070
+ const { workspace, exportSettings, target, onPhase } = args;
180012
180071
  const { projectId, versionId } = workspace.config;
180013
180072
  let started = performance.now();
180014
180073
  const lap = () => {
@@ -180025,6 +180084,7 @@ function exportUnityProject(args) {
180025
180084
  })
180026
180085
  );
180027
180086
  const read = lap();
180087
+ onPhase(`Building export from ${Object.keys(records2).length} records\u2026`);
180028
180088
  const unityExport = buildProjectUnityExport(document);
180029
180089
  const files = pulledExportFiles(workspace, Object.keys(unityExport.files));
180030
180090
  const recordHashes = {};
@@ -180039,6 +180099,7 @@ function exportUnityProject(args) {
180039
180099
  });
180040
180100
  const projectDiagnostics2 = buildUnityExportProjectDiagnostics(document);
180041
180101
  const build = lap();
180102
+ if (!skipCodegen) onPhase("Generating Unity types\u2026");
180042
180103
  const generated = skipCodegen ? null : generateUnityTypes(unityExport, buildUnityCodegenDocs(document));
180043
180104
  const codegen = lap();
180044
180105
  const diagnostics = [
@@ -180090,6 +180151,7 @@ ${errors.map((error) => ` ${formatDiagnostic(error)}`).join("\n")}`
180090
180151
  projectJsonPath,
180091
180152
  dataFiles.get(projectJsonPath)
180092
180153
  );
180154
+ onPhase(`Writing changed files into ${target.unityProjectRoot}\u2026`);
180093
180155
  const written = [];
180094
180156
  let unchanged = 0;
180095
180157
  for (const [path, content] of [...dataFiles, ...localizationFiles.writes]) {
@@ -180938,7 +181000,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
180938
181000
  async function main() {
180939
181001
  const args = parseArgs(process.argv.slice(2));
180940
181002
  if (args.command === "--version") {
180941
- console.log("0.69.0");
181003
+ console.log("0.69.1");
180942
181004
  return;
180943
181005
  }
180944
181006
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
@@ -37258,6 +37258,17 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
37258
37258
  targetType,
37259
37259
  schemaClass
37260
37260
  );
37261
+ if (memberType.kind === "action") {
37262
+ return {
37263
+ schemaKey: member.schemaKey ?? member.name,
37264
+ memberId: member.id,
37265
+ valuePointer: this.resolveReplayedListenerSet(
37266
+ entry.value,
37267
+ memberType,
37268
+ scope
37269
+ )
37270
+ };
37271
+ }
37261
37272
  const value = this.resolveExpression(entry.value, scope, memberType);
37262
37273
  if (!isNeoScriptTypeAssignable(value.type, memberType, this.project)) {
37263
37274
  throw new CompileError(
@@ -37272,6 +37283,43 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
37272
37283
  };
37273
37284
  });
37274
37285
  }
37286
+ /**
37287
+ * P62 §2. A stored action slot replays as the listener list it stores. Each
37288
+ * entry resolves to the same static (member, row) identity the authored list
37289
+ * lowers to. Executable script cannot reassign a listener set (§3.2).
37290
+ */
37291
+ resolveReplayedListenerSet(expression, actionType, scope) {
37292
+ if (this.context.storedConstructionReplay !== true) {
37293
+ throw new CompileError(
37294
+ `Cannot assign to ${this.describe(actionType)}; a listener set is not reassignable from script. Use '+=' to add a listener or '-=' to remove one.`,
37295
+ expression.pos
37296
+ );
37297
+ }
37298
+ if (expression.kind !== "litList") {
37299
+ throw new CompileError(
37300
+ `A stored ${this.describe(actionType)} replays as a bracketed listener list.`,
37301
+ expression.pos
37302
+ );
37303
+ }
37304
+ const listeners = [];
37305
+ for (const element of expression.elements) {
37306
+ const listener = this.resolveActionListener(element, actionType, scope);
37307
+ if (listener.type !== "value" /* Value */) {
37308
+ throw new CompileError(
37309
+ "A stored listener must name a stable (member, row) target.",
37310
+ element.pos
37311
+ );
37312
+ }
37313
+ listeners.push(listener.value.value);
37314
+ }
37315
+ return {
37316
+ type: "value" /* Value */,
37317
+ value: {
37318
+ typeInfo: toWireType(actionType, this.project),
37319
+ value: { listeners }
37320
+ }
37321
+ };
37322
+ }
37275
37323
  resolveTileConversion(callee, args, typeArguments, scope, pos) {
37276
37324
  const receiver = this.resolveExpression(callee.receiver, scope);
37277
37325
  const owner = receiver.type.kind === "named" ? this.project.typeById.get(receiver.type.typeId) : void 0;
@@ -43398,7 +43446,6 @@ var project_document_read_exports = {};
43398
43446
  __export(project_document_read_exports, {
43399
43447
  DOCUMENT_MANIFEST_PAGE_SIZE: () => DOCUMENT_MANIFEST_PAGE_SIZE,
43400
43448
  assembleProjectDocumentRaw: () => assembleProjectDocumentRaw,
43401
- createDocumentPageReader: () => createDocumentPageReader,
43402
43449
  fetchProjectDocumentPartitionRecords: () => fetchProjectDocumentPartitionRecords,
43403
43450
  fetchProjectDocumentRawChunked: () => fetchProjectDocumentRawChunked,
43404
43451
  fetchProjectDocumentRawChunkedOnce: () => fetchProjectDocumentRawChunkedOnce,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.69.0",
3
+ "version": "0.69.1",
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.69.0 -->
12
+ <!-- reviewed-through-cli: 0.69.1 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -110,7 +110,7 @@ wrappers.
110
110
  The marker near the top of `SKILL.md` must exactly match the package version:
111
111
 
112
112
  ```html
113
- <!-- reviewed-through-cli: 0.69.0 -->
113
+ <!-- reviewed-through-cli: 0.69.1 -->
114
114
  ```
115
115
 
116
116
  The quoted version above is checked too, so this instruction cannot go stale