@neocompose/cli 0.51.0 → 0.51.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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.51.1] - 2026-09-16
4
+
5
+ - Refresh compiler-only changes with ordinary `neo pull` before compiling the local workspace. Preserve authored source, local edits, custom paths, and managed binaries while adopting the current server compilation.
6
+ - Refuse a normal pull when the baseline is missing but authored source files remain, preventing duplicate declarations and overwritten local edits. An explicit forced refresh recovers custom file paths from the existing declarations.
7
+
3
8
  ## [0.51.0] - 2026-09-15
4
9
 
5
10
  Compiler revision 14 requires Unity SDK 0.37.0. The server migration recompiles stored scripts and retained history.
package/dist/neo.mjs CHANGED
@@ -87329,6 +87329,44 @@ function stampUILogicBody(logic, bodyKey) {
87329
87329
  body.compilerRevision = NEOSCRIPT_COMPILER_REVISION;
87330
87330
  return true;
87331
87331
  }
87332
+ function stampUILogicBodies(recordKind, data) {
87333
+ const record5 = asRecord2(data);
87334
+ if (record5 === null) return false;
87335
+ if (recordKind === "dialogue-group") {
87336
+ return stampConditionList(record5.conditions);
87337
+ }
87338
+ if (recordKind !== "dialogue-node") return false;
87339
+ let changed = stampConditionList(record5.conditions);
87340
+ if (Array.isArray(record5.actions)) {
87341
+ for (const actionValue2 of record5.actions) {
87342
+ const logic = asRecord2(asRecord2(actionValue2)?.logic);
87343
+ changed = stampUILogicBody(logic, "action") || changed;
87344
+ }
87345
+ }
87346
+ if (Array.isArray(record5.outcomes)) {
87347
+ for (const outcomeValue of record5.outcomes) {
87348
+ changed = stampConditionList(asRecord2(outcomeValue)?.conditions) || changed;
87349
+ }
87350
+ }
87351
+ const options = asRecord2(record5.optionSettings)?.options;
87352
+ if (Array.isArray(options)) {
87353
+ for (const optionValue of options) {
87354
+ const settings = asRecord2(asRecord2(optionValue)?.settings);
87355
+ if (settings === null) continue;
87356
+ changed = stampConditionList(settings.conditions) || changed;
87357
+ changed = stampConditionList(settings.selectableConditions) || changed;
87358
+ }
87359
+ }
87360
+ return changed;
87361
+ }
87362
+ function stampConditionList(value) {
87363
+ if (!Array.isArray(value)) return false;
87364
+ let changed = false;
87365
+ for (const conditionValue of value) {
87366
+ changed = stampUILogicBody(asRecord2(conditionValue), "getter") || changed;
87367
+ }
87368
+ return changed;
87369
+ }
87332
87370
  function asRecord2(value) {
87333
87371
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
87334
87372
  return null;
@@ -126661,6 +126699,14 @@ var init_convex = __esm({
126661
126699
  // src/project-source/source-layout.ts
126662
126700
  import { readFileSync as readFileSync8 } from "node:fs";
126663
126701
  import { relative as relative4, sep as sep4 } from "node:path";
126702
+ function workspaceForSourceLayoutV4(workspace, document) {
126703
+ if (Object.keys(workspace.state.records).length > 0) return workspace;
126704
+ const records2 = {};
126705
+ for (const [key, record5] of document.records) {
126706
+ if (!record5.deleted) records2[key] = record5;
126707
+ }
126708
+ return { ...workspace, state: { ...workspace.state, records: records2 } };
126709
+ }
126664
126710
  function readWorkspaceRecordFilesV4(workspace) {
126665
126711
  const paths = /* @__PURE__ */ new Map();
126666
126712
  const keysById = /* @__PURE__ */ new Map();
@@ -127446,17 +127492,7 @@ import {
127446
127492
  import { dirname as dirname7, join as join8, relative as relative5, sep as sep5 } from "node:path";
127447
127493
  function prepareWorkspaceResetV4(workspace, document, options = {}) {
127448
127494
  const emissionRecords = options.regenerateSourceNames ? regenerateDialogueSourceNamesV4(document.records) : document.records;
127449
- let layoutWorkspace = workspace;
127450
- if (Object.keys(workspace.state.records).length === 0) {
127451
- const records2 = {};
127452
- for (const [key, record5] of document.records) {
127453
- if (!record5.deleted) records2[key] = record5;
127454
- }
127455
- layoutWorkspace = {
127456
- ...workspace,
127457
- state: { ...workspace.state, records: records2 }
127458
- };
127459
- }
127495
+ const layoutWorkspace = workspaceForSourceLayoutV4(workspace, document);
127460
127496
  const status = listProjectSourceFilesV4(workspace.root).length === 0 ? null : readWorkspaceLayoutStatusV4(layoutWorkspace);
127461
127497
  const localValues = status?.localValues ?? workspace.state.localValues;
127462
127498
  const emitted = emitProjectDocumentFilesV4(emissionRecords, {
@@ -127874,6 +127910,40 @@ var init_static_value_seed_records = __esm({
127874
127910
  }
127875
127911
  });
127876
127912
 
127913
+ // src/project-sync/compiler-metadata.ts
127914
+ function compilerMetadataChangedKeys(previous, document, changedKeys) {
127915
+ const keys = changedKeys ?? /* @__PURE__ */ new Set([...Object.keys(previous), ...document.records.keys()]);
127916
+ const refreshed = /* @__PURE__ */ new Set();
127917
+ for (const key of keys) {
127918
+ const before = previous[key];
127919
+ const after = document.records.get(key);
127920
+ if (before === void 0 || after === void 0 || after.deleted || before.recordId !== after.recordId || before.recordKind !== after.recordKind || !isProjectRecordKind(after.recordKind))
127921
+ return null;
127922
+ if (before.contentHash === after.contentHash) continue;
127923
+ if (!isObjectRecord2(before.data) || !isObjectRecord2(after.data) || !canonicallyEqual(
127924
+ authoredCompilerInput(after.recordKind, before.data),
127925
+ authoredCompilerInput(after.recordKind, after.data)
127926
+ ))
127927
+ return null;
127928
+ refreshed.add(key);
127929
+ }
127930
+ return refreshed.size === 0 ? null : refreshed;
127931
+ }
127932
+ function authoredCompilerInput(kind, data) {
127933
+ const result = stripClientDerivedBodies(kind, data);
127934
+ stampUILogicBodies(kind, result);
127935
+ if (isObjectRecord2(result)) delete result.updatedAt;
127936
+ return result;
127937
+ }
127938
+ var init_compiler_metadata = __esm({
127939
+ "src/project-sync/compiler-metadata.ts"() {
127940
+ "use strict";
127941
+ init_project2();
127942
+ init_neo_script_derived_bodies();
127943
+ init_projection();
127944
+ }
127945
+ });
127946
+
127877
127947
  // src/commands/pull.ts
127878
127948
  var pull_exports = {};
127879
127949
  __export(pull_exports, {
@@ -127903,7 +127973,15 @@ async function runPull(workspace, options, progress = spinner("Checking local wo
127903
127973
  async function runNormalPull(workspace, options, progress) {
127904
127974
  const destructive = options.force;
127905
127975
  const hasBaseline = Object.keys(workspace.state.records).length > 0;
127976
+ if (!destructive && !hasBaseline && listProjectSourceFilesV4(workspace.root).length > 0) {
127977
+ throw new Error(
127978
+ "Cannot merge existing Neo sources without a pull baseline in .neo/state.json. Restore that file to preserve local edits, or back up your sources and run `neo pull --force` to replace them with the server version while preserving their paths."
127979
+ );
127980
+ }
127906
127981
  const mainLocale = workspaceMainLocale(workspace.state.records);
127982
+ const hadConflicts = Object.values(workspace.state.records).some(
127983
+ (record5) => record5.conflictServerHash !== void 0 || record5.conflictFiles !== void 0 || record5.projectBinary?.conflict !== void 0
127984
+ );
127907
127985
  if (acceptSourceEquivalentConflictBases(workspace, mainLocale) > 0) {
127908
127986
  writeWorkspaceState(workspace.root, workspace.state);
127909
127987
  }
@@ -127961,6 +128039,28 @@ async function runNormalPull(workspace, options, progress) {
127961
128039
  document = await fetchProjectDocument(workspace);
127962
128040
  nextCursor = document.revision === void 0 ? null : cursorFromRevision(document.revision);
127963
128041
  }
128042
+ const compilerRefreshKeys = !destructive && hasBaseline && !hadConflicts && !options.regenerateSourceNames ? compilerMetadataChangedKeys(
128043
+ workspace.state.records,
128044
+ document,
128045
+ changedRecordKeys
128046
+ ) : null;
128047
+ if (compilerRefreshKeys !== null) {
128048
+ if (changedRecordKeys === null) {
128049
+ await updateFullPullSyncMetadata(workspace, document);
128050
+ observedHeadTransactionHash = workspace.state.headTransactionHash;
128051
+ observedDerivedEdgeVersion = workspace.state.derivedEdgeVersion;
128052
+ }
128053
+ finishRecordOnlyDeltaPull({
128054
+ workspace,
128055
+ document,
128056
+ changedRecordKeys: compilerRefreshKeys,
128057
+ nextCursor,
128058
+ observedHeadTransactionHash,
128059
+ observedDerivedEdgeVersion,
128060
+ progress
128061
+ });
128062
+ return;
128063
+ }
127964
128064
  let localByKey = /* @__PURE__ */ new Map();
127965
128065
  let localStatus = null;
127966
128066
  if (hasBaseline && !destructive) {
@@ -128151,18 +128251,7 @@ async function runResetPull(workspace, progress) {
128151
128251
  if (record5) record5.projectBinary = state;
128152
128252
  }
128153
128253
  progress.update("Finalizing pull state\u2026");
128154
- try {
128155
- const convex = await createConvexClient(workspace);
128156
- const signal = await convex.query(api2.projectExportData.schemaSignal, {
128157
- projectId: workspace.config.projectId,
128158
- versionId: workspace.config.versionId
128159
- });
128160
- workspace.state.headTransactionHash = document.revision !== void 0 && signal?.transactionId === document.revision.latestTransactionId ? signal.transactionHash : null;
128161
- workspace.state.derivedEdgeVersion = signal?.derivedEdgeVersion ?? null;
128162
- } catch {
128163
- workspace.state.headTransactionHash = null;
128164
- delete workspace.state.derivedEdgeVersion;
128165
- }
128254
+ await updateFullPullSyncMetadata(workspace, document);
128166
128255
  if (document.revision !== void 0) {
128167
128256
  workspace.state.documentRevisionCursor = mutableCursor(
128168
128257
  cursorFromRevision(document.revision)
@@ -128234,11 +128323,12 @@ async function finishFormat4Pull(args) {
128234
128323
  ([key, record5]) => localRecords.get(key) === record5 ? [] : [key]
128235
128324
  )
128236
128325
  );
128237
- const recordFiles = readWorkspaceRecordFilesV4(workspace);
128326
+ const layoutWorkspace = workspaceForSourceLayoutV4(workspace, document);
128327
+ const recordFiles = readWorkspaceRecordFilesV4(layoutWorkspace);
128238
128328
  for (const [key, record5] of localStatus?.reconstructed ?? []) {
128239
128329
  if (record5.file != null) recordFiles.set(key, record5.file);
128240
128330
  }
128241
- const layoutStatus = localStatus ?? (destructive ? readWorkspaceLayoutStatusV4(workspace) : null);
128331
+ const layoutStatus = localStatus ?? (destructive ? readWorkspaceLayoutStatusV4(layoutWorkspace) : null);
128242
128332
  const localValues = layoutStatus?.localValues ?? workspace.state.localValues;
128243
128333
  const binaryPaths = workspaceBinaryPathsV4(
128244
128334
  workspace,
@@ -128256,6 +128346,16 @@ async function finishFormat4Pull(args) {
128256
128346
  buildEmitRecordSet(document, plans, "server"),
128257
128347
  { recordFiles, localValues, binaryPaths }
128258
128348
  );
128349
+ const resolveSourcePath = createWorkspaceSourcePathResolver(workspace.root);
128350
+ if (!destructive && Object.keys(workspace.state.records).length === 0) {
128351
+ for (const file of localResult.files) {
128352
+ if (existsSync8(resolveSourcePath(file.path))) {
128353
+ throw new Error(
128354
+ `Cannot replace existing source ${JSON.stringify(file.path)} without a pull baseline in .neo/state.json. Restore that file to preserve local edits, or back up your sources and run \`neo pull --force\` to replace them with the server version.`
128355
+ );
128356
+ }
128357
+ }
128358
+ }
128259
128359
  const binaries = await pullProjectBinariesV4({
128260
128360
  workspace,
128261
128361
  document,
@@ -128309,7 +128409,6 @@ async function finishFormat4Pull(args) {
128309
128409
  for (const key of regeneratedRecordKeys) {
128310
128410
  for (const path of localRecordFiles.get(key) ?? []) rewritePaths.add(path);
128311
128411
  }
128312
- const resolveSourcePath = createWorkspaceSourcePathResolver(workspace.root);
128313
128412
  const markedPaths = /* @__PURE__ */ new Set();
128314
128413
  let written = 0;
128315
128414
  for (const path of emittedPaths) {
@@ -128415,18 +128514,7 @@ async function finishFormat4Pull(args) {
128415
128514
  if (observedHeadTransactionHash !== void 0) {
128416
128515
  workspace.state.headTransactionHash = observedHeadTransactionHash;
128417
128516
  } else {
128418
- try {
128419
- const convex = await createConvexClient(workspace);
128420
- const signal = await convex.query(api2.projectExportData.schemaSignal, {
128421
- projectId: workspace.config.projectId,
128422
- versionId: workspace.config.versionId
128423
- });
128424
- workspace.state.headTransactionHash = document.revision !== void 0 && signal?.transactionId === document.revision.latestTransactionId ? signal.transactionHash : null;
128425
- workspace.state.derivedEdgeVersion = signal?.derivedEdgeVersion ?? null;
128426
- } catch {
128427
- workspace.state.headTransactionHash = null;
128428
- delete workspace.state.derivedEdgeVersion;
128429
- }
128517
+ await updateFullPullSyncMetadata(workspace, document);
128430
128518
  }
128431
128519
  if (observedDerivedEdgeVersion !== void 0) {
128432
128520
  workspace.state.derivedEdgeVersion = observedDerivedEdgeVersion;
@@ -128598,6 +128686,20 @@ function finishRecordOnlyDeltaPull(args) {
128598
128686
  `Pulled ${args.changedRecordKeys.size} changed record(s) \u2014 0 file(s) updated \u2014 0 removed.`
128599
128687
  );
128600
128688
  }
128689
+ async function updateFullPullSyncMetadata(workspace, document) {
128690
+ try {
128691
+ const convex = await createConvexClient(workspace);
128692
+ const signal = await convex.query(api2.projectExportData.schemaSignal, {
128693
+ projectId: workspace.config.projectId,
128694
+ versionId: workspace.config.versionId
128695
+ });
128696
+ workspace.state.headTransactionHash = document.revision !== void 0 && signal?.transactionId === document.revision.latestTransactionId ? signal.transactionHash : null;
128697
+ workspace.state.derivedEdgeVersion = signal?.derivedEdgeVersion ?? null;
128698
+ } catch {
128699
+ workspace.state.headTransactionHash = null;
128700
+ delete workspace.state.derivedEdgeVersion;
128701
+ }
128702
+ }
128601
128703
  function cursorFromRevision(revision) {
128602
128704
  return {
128603
128705
  readBase: revision.readBase,
@@ -128755,6 +128857,7 @@ var init_pull = __esm({
128755
128857
  init_static_value_seed_records();
128756
128858
  init_static_member_ownership_recovery();
128757
128859
  init_value_base_desync();
128860
+ init_compiler_metadata();
128758
128861
  }
128759
128862
  });
128760
128863
 
@@ -136399,7 +136502,7 @@ var init_registry2 = __esm({
136399
136502
  "schema-contract/registry.mjs"() {
136400
136503
  "use strict";
136401
136504
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
136402
- cliVersion: "0.51.0",
136505
+ cliVersion: "0.51.1",
136403
136506
  projectFileUploadBatchSize: 32,
136404
136507
  documentRecords: {
136405
136508
  member: {
@@ -143628,7 +143731,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
143628
143731
  async function main() {
143629
143732
  const args = parseArgs(process.argv.slice(2));
143630
143733
  if (args.command === "--version") {
143631
- console.log("0.51.0");
143734
+ console.log("0.51.1");
143632
143735
  return;
143633
143736
  }
143634
143737
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.51.0",
3
+ "version": "0.51.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.51.0 -->
12
+ <!-- reviewed-through-cli: 0.51.1 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -90,7 +90,7 @@ wrappers.
90
90
  The marker near the top of `SKILL.md` must exactly match the package version:
91
91
 
92
92
  ```html
93
- <!-- reviewed-through-cli: 0.51.0 -->
93
+ <!-- reviewed-through-cli: 0.51.1 -->
94
94
  ```
95
95
 
96
96
  The quoted version above is checked too, so this instruction cannot go stale
@@ -40,9 +40,17 @@ without compiling it; use `neo status` to inspect local edits and diagnostics.
40
40
  An undo/reset metadata change invalidates the cursor and causes a safe
41
41
  full-snapshot fallback.
42
42
 
43
- If a compiler upgrade rejects the cached snapshot, save local edits outside the
44
- checkout, run `neo pull --force`, then reapply the edits. Normal pull cannot
45
- merge against a snapshot whose compiler revision is no longer supported.
43
+ After a compiler upgrade, run `neo pull` with `.neo/state.json` intact. When
44
+ remote changes affect only compiler-owned bodies or timestamps, pull refreshes
45
+ the cached records before local compilation and leaves authored files and
46
+ managed binaries untouched. It handles both changed-record pulls and full
47
+ snapshot refreshes. Added or deleted records, authored changes, and unresolved
48
+ conflicts still require the normal merge path.
49
+
50
+ Keep `.neo/state.json`: it holds the merge baseline and source placements.
51
+ Normal pull refuses existing production sources without that baseline. To
52
+ deliberately replace a checkout whose state is gone, back it up first and use
53
+ `neo pull --force`; the CLI recovers custom paths from the remaining declarations.
46
54
  The force pull replaces local source with the server version.
47
55
 
48
56
  Pull pins every page and snapshot batch to one published generation and logical