@neocompose/cli 0.70.0 → 0.70.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.70.1] - 2026-10-06
4
+
5
+ - `neo push` can upload a project file renamed with `@settings(name: ...)`. It used to stage the upload under the file's basename, so the commit failed with "Staged project file … did not match its source name".
6
+ - A project file's `@settings(name: ...)` must end with its file's extension, for example `@settings(name: "ClinkingCoin1.mp3")` for `clinkingcoin1.mp3`. A pull on another machine rebuilds the file's path from that name. The symbol error now says the name needs that extension.
7
+ - Push and pull keep `@settings(name: ...)` on a renamed file, and a new file keeps the path you gave it. Before, the rewrite after a push dropped the name and rebuilt the path from it.
8
+
3
9
  ## [0.70.0] - 2026-10-06
4
10
 
5
11
  - `neo export` exports the working copy, local edits included, with no network. Unity sees an edit as soon as it's saved, before `neo push`. The export says whether it holds only the pulled head ("Clean") or how many local changes it includes ("Local"). A local export is marked in `metadata.localExport`; SDK 0.59.0 refuses player builds from it and keeps play-mode saves on disk instead of the server. A clean `neo export` or a push clears the mark. A local export still stops, writing nothing, on an error diagnostic or on a project file whose pulled bytes aren't on disk yet; run `neo pull` first.
package/dist/neo.mjs CHANGED
@@ -31811,7 +31811,7 @@ var init_registry = __esm({
31811
31811
  "../packages/manifest/schema-contract/registry.mjs"() {
31812
31812
  "use strict";
31813
31813
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
31814
- cliVersion: "0.70.0",
31814
+ cliVersion: "0.70.1",
31815
31815
  projectFileUploadBatchSize: 32,
31816
31816
  documentRecords: {
31817
31817
  member: {
@@ -60588,7 +60588,7 @@ function validateProjectFileDeclarationSymbolsV4(declarations) {
60588
60588
  const expected = expectedSymbols.get(declaration.recordId);
60589
60589
  if (expected === void 0 || declaration.symbol === expected) continue;
60590
60590
  throw new Error(
60591
- `Project file ${declaration.symbol} must use registry symbol ${expected}, derived from its persisted name ${JSON.stringify(declaration.persistedName)}. Rename the declaration or set @settings(name: ...) so the runtime derives the authored symbol.`
60591
+ `Project file ${declaration.symbol} must use registry symbol ${expected}, derived from its persisted name ${JSON.stringify(declaration.persistedName)}. Rename the declaration, or set @settings(name: ...) to a file name ending in ${JSON.stringify(sourceExtension(declaration.persistedName))} that derives ${declaration.symbol}.`
60592
60592
  );
60593
60593
  }
60594
60594
  }
@@ -60621,7 +60621,6 @@ function emitProjectFileRegistrySourcesV4(records2, binaryPaths = /* @__PURE__ *
60621
60621
  const registryType = kind === "image" ? "ImageRegistry" : "AudioClipRegistry";
60622
60622
  const registryName = kind === "image" ? "Images" : "AudioClips";
60623
60623
  const valueType = kind === "image" ? "NeoImage" : "NeoAudioClip";
60624
- const directory = kind === "image" ? "Files/Images" : "Files/AudioClips";
60625
60624
  const entries = liveFiles.filter(({ data }) => data.fileType === kind).sort((left, right) => {
60626
60625
  const leftSymbol = symbols.get(left.record.recordId) ?? "";
60627
60626
  const rightSymbol = symbols.get(right.record.recordId) ?? "";
@@ -60638,12 +60637,16 @@ function emitProjectFileRegistrySourcesV4(records2, binaryPaths = /* @__PURE__ *
60638
60637
  const templateName = templateId === null ? null : (kind === "image" ? textureTemplates : audioTemplates).get(
60639
60638
  templateId
60640
60639
  );
60641
- const annotation2 = templateName ? ` @settings(template: ${templateName})
60642
- ` : "";
60643
60640
  const path = normalizeTrustedProjectBinaryPath(
60644
- binaryPaths.get(record6.recordId) ?? `${directory}/${safeFileName(String(data.name))}`,
60641
+ binaryPaths.get(record6.recordId) ?? canonicalProjectBinaryPathV4(data),
60645
60642
  kind
60646
60643
  );
60644
+ const options = [
60645
+ ...templateName ? [`template: ${templateName}`] : [],
60646
+ ...data.name === sourceBasename(path) ? [] : [`name: ${JSON.stringify(data.name)}`]
60647
+ ];
60648
+ const annotation2 = options.length === 0 ? "" : ` @settings(${options.join(", ")})
60649
+ `;
60647
60650
  return ` @id(${JSON.stringify(record6.recordId)})
60648
60651
  ${annotation2} ${valueType} ${symbol} = new(${JSON.stringify(path)});`;
60649
60652
  });
@@ -60665,10 +60668,7 @@ function projectFileDeclarationsV4(analysis) {
60665
60668
  );
60666
60669
  }
60667
60670
  function lowerTrustedProjectFileRegistrySourcesV4(state, analysis, trustedPendingFiles = /* @__PURE__ */ new Map()) {
60668
- const templateIds = fileTemplateIdsByName(analysis);
60669
- const declarations = analysis.files.registries.flatMap(
60670
- (registry) => parseRegistry(registry, templateIds)
60671
- );
60671
+ const declarations = projectFileDeclarationsV4(analysis);
60672
60672
  validateProjectFileDeclarationSymbolsV4(declarations);
60673
60673
  const seenPaths = /* @__PURE__ */ new Map();
60674
60674
  const seenSymbols = /* @__PURE__ */ new Map();
@@ -60848,7 +60848,7 @@ function canonicalProjectBinaryPathV4(data) {
60848
60848
  if (typeof data.name !== "string") {
60849
60849
  throw new Error("Project file name is missing.");
60850
60850
  }
60851
- return `${kind === "image" ? "Files/Images" : "Files/AudioClips"}/${safeFileName(data.name)}`;
60851
+ return `${kind === "image" ? "Files/Images" : "Files/AudioClips"}/${safeProjectFileName(data.name)}`;
60852
60852
  }
60853
60853
  function fileTemplateIdsByName(analysis) {
60854
60854
  const templateIds = /* @__PURE__ */ new Map();
@@ -60900,13 +60900,22 @@ function parseRegistry(registry, templateIds) {
60900
60900
  const settings = parseFileSettings(match[2], templateIds);
60901
60901
  const path = JSON.parse(match[5]);
60902
60902
  const recordId = match[1] ? JSON.parse(match[1]) : `__pending__:project-file:${encodeURIComponent(registry.source.uri)}:${registry.source.range.start.line}:${encodeURIComponent(match[4])}`;
60903
+ const fileName2 = sourceBasename(path);
60904
+ if (settings.name !== null) {
60905
+ const extension = sourceExtension(fileName2);
60906
+ if (!settings.name.toLowerCase().endsWith(extension.toLowerCase())) {
60907
+ throw new Error(
60908
+ `Project file ${match[4]} @settings(name: ${JSON.stringify(settings.name)}) must end with ${JSON.stringify(extension)}, the extension of ${JSON.stringify(path)}.`
60909
+ );
60910
+ }
60911
+ }
60903
60912
  result.push({
60904
60913
  recordId,
60905
60914
  symbol: match[4],
60906
60915
  path,
60907
60916
  kind,
60908
60917
  templateId: settings.templateId,
60909
- persistedName: settings.name ?? sourceBasename(path),
60918
+ persistedName: settings.name ?? fileName2,
60910
60919
  sourceFile: registry.source.uri,
60911
60920
  sourceLine: registry.source.range.start.line + 1
60912
60921
  });
@@ -60992,7 +61001,7 @@ function templateNames(records2, kind) {
60992
61001
  })
60993
61002
  );
60994
61003
  }
60995
- function safeFileName(value) {
61004
+ function safeProjectFileName(value) {
60996
61005
  const fileName2 = sourceBasename(value).replace(/[\\/]/g, "-");
60997
61006
  if (fileName2 === "." || fileName2 === ".." || fileName2.length === 0) {
60998
61007
  throw new Error(`Invalid project filename ${JSON.stringify(value)}.`);
@@ -61002,6 +61011,9 @@ function safeFileName(value) {
61002
61011
  function sourceBasename(value) {
61003
61012
  return value.replaceAll("\\", "/").split("/").at(-1) ?? "";
61004
61013
  }
61014
+ function sourceExtension(value) {
61015
+ return /\.[^.]+$/.exec(value)?.[0] ?? "";
61016
+ }
61005
61017
  function optionalString(value) {
61006
61018
  return typeof value === "string" ? value : null;
61007
61019
  }
@@ -136733,10 +136745,7 @@ import {
136733
136745
  } from "node:fs";
136734
136746
  import { basename, dirname as dirname7, extname, join as join6, relative as relative2, sep as sep2 } from "node:path";
136735
136747
  function lowerProjectFileRegistrySourcesV4(root, state, analysis, options = {}) {
136736
- const templateIds = fileTemplateIdsByName2(analysis);
136737
- const declarations = analysis.files.registries.flatMap(
136738
- (registry) => parseRegistry2(registry, templateIds)
136739
- );
136748
+ const declarations = projectFileDeclarationsV4(analysis);
136740
136749
  validateProjectFileDeclarationSymbolsV4(declarations);
136741
136750
  const seenPaths = /* @__PURE__ */ new Map();
136742
136751
  const seenSymbols = /* @__PURE__ */ new Map();
@@ -136821,14 +136830,8 @@ function lowerProjectFileRegistrySourcesV4(root, state, analysis, options = {})
136821
136830
  };
136822
136831
  });
136823
136832
  }
136824
- function projectFileDeclarationsV42(analysis) {
136825
- const templateIds = fileTemplateIdsByName2(analysis);
136826
- return analysis.files.registries.flatMap(
136827
- (registry) => parseRegistry2(registry, templateIds)
136828
- );
136829
- }
136830
136833
  function lowerDiscoveredProjectBinariesV4(root, state, analysis) {
136831
- const explicit = projectFileDeclarationsV42(analysis);
136834
+ const explicit = projectFileDeclarationsV4(analysis);
136832
136835
  const project = Object.values(state).find(
136833
136836
  (entry) => entry.recordKind === "project" && isObjectRecord2(entry.data)
136834
136837
  );
@@ -136869,7 +136872,7 @@ function lowerDiscoveredProjectBinariesV4(root, state, analysis) {
136869
136872
  });
136870
136873
  }
136871
136874
  function inspectProjectBinaryStatusV4(root, state, analysis) {
136872
- const explicit = projectFileDeclarationsV42(analysis);
136875
+ const explicit = projectFileDeclarationsV4(analysis);
136873
136876
  const result = explicit.map((declaration) => {
136874
136877
  const baseState = state[`project-file:${declaration.recordId}`];
136875
136878
  const data = isObjectRecord2(baseState?.data) ? baseState.data : {};
@@ -136927,16 +136930,6 @@ function inspectProjectBinaryStatusV4(root, state, analysis) {
136927
136930
  function pendingDiscoveredFileId(path) {
136928
136931
  return `__pending__:project-file:drag:${encodeURIComponent(normalizeSlash2(path))}`;
136929
136932
  }
136930
- function canonicalProjectBinaryPathV42(data) {
136931
- if (!isObjectRecord2(data))
136932
- throw new Error("Project file metadata must be an object.");
136933
- if (data.fileType !== "image" && data.fileType !== "audio")
136934
- throw new Error("Project file metadata has an unsupported fileType.");
136935
- if (typeof data.name !== "string")
136936
- throw new Error("Project file metadata has no filename.");
136937
- const directory = data.fileType === "image" ? "Files/Images" : "Files/AudioClips";
136938
- return `${directory}/${safeFileName2(data.name)}`;
136939
- }
136940
136933
  function discoverProjectBinariesV4(root, explicit, ignoredPaths = []) {
136941
136934
  const explicitPaths = new Set(
136942
136935
  [...explicit.map((entry) => entry.path), ...ignoredPaths].map(
@@ -137012,124 +137005,11 @@ function writeBinaryConflictArtifactV4(root, fileId, fileName2, bytes, expectedS
137012
137005
  "conflicts",
137013
137006
  "files",
137014
137007
  safePathSegment2(fileId),
137015
- safeFileName2(fileName2)
137008
+ safeProjectFileName(fileName2)
137016
137009
  );
137017
137010
  writeVerifiedBinaryDownloadV4(destination, bytes, expectedSha256);
137018
137011
  return normalizeSlash2(relative2(root, destination));
137019
137012
  }
137020
- function fileTemplateIdsByName2(analysis) {
137021
- const templateIds = /* @__PURE__ */ new Map();
137022
- for (const global of analysis.configuration.globals) {
137023
- const kind = global.type.name === "TextureTemplate" ? "texture-template" : global.type.name === "AudioTemplate" ? "audio-template" : null;
137024
- if (kind === null) continue;
137025
- templateIds.set(
137026
- global.name,
137027
- global.id ?? `__pending__:${kind}:${encodeURIComponent(global.source.uri)}:${global.source.range.start.line}:${global.source.range.start.character}:${encodeURIComponent(global.name)}`
137028
- );
137029
- }
137030
- return templateIds;
137031
- }
137032
- function parseRegistry2(registry, templateIds) {
137033
- const kind = registry.type.name === "ImageRegistry" ? "image" : registry.type.name === "AudioClipRegistry" ? "audio" : null;
137034
- if (kind === null)
137035
- throw new Error(
137036
- `Unsupported project file registry type ${registry.type.name}.`
137037
- );
137038
- const expectedName = kind === "image" ? "Images" : "AudioClips";
137039
- if (registry.name !== expectedName)
137040
- throw new Error(
137041
- `${registry.type.name} must use the canonical global name ${expectedName}.`
137042
- );
137043
- const body = objectInitializerBody2(registry.initializer, registry.type.name);
137044
- const result = [];
137045
- let cursor = 0;
137046
- while (cursor < body.length) {
137047
- const whitespace = /^(?:\s|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/.exec(
137048
- body.slice(cursor)
137049
- );
137050
- if (whitespace) cursor += whitespace[0].length;
137051
- if (cursor >= body.length) break;
137052
- REGISTRY_ENTRY2.lastIndex = cursor;
137053
- const match = REGISTRY_ENTRY2.exec(body);
137054
- if (!match)
137055
- throw new Error(
137056
- `${registry.type.name} ${registry.name} contains an invalid file declaration near ${JSON.stringify(body.slice(cursor, cursor + 80))}.`
137057
- );
137058
- const entryType = match[3];
137059
- if (kind === "image" && entryType !== "NeoImage" || kind === "audio" && entryType !== "NeoAudioClip") {
137060
- throw new Error(
137061
- `${registry.type.name} cannot contain ${entryType} declarations.`
137062
- );
137063
- }
137064
- const settings = parseFileSettings2(match[2], templateIds);
137065
- const path = JSON.parse(match[5]);
137066
- const recordId = match[1] ? JSON.parse(match[1]) : `__pending__:project-file:${encodeURIComponent(registry.source.uri)}:${registry.source.range.start.line}:${encodeURIComponent(match[4])}`;
137067
- result.push({
137068
- recordId,
137069
- symbol: match[4],
137070
- path,
137071
- kind,
137072
- templateId: settings.templateId,
137073
- persistedName: settings.name ?? basename(path),
137074
- sourceFile: registry.source.uri,
137075
- sourceLine: registry.source.range.start.line + 1
137076
- });
137077
- cursor = REGISTRY_ENTRY2.lastIndex;
137078
- }
137079
- return result;
137080
- }
137081
- function parseFileSettings2(source, templateIds) {
137082
- if (!source) return { templateId: null, name: null };
137083
- const expression = parseExpression(`new(${source})`);
137084
- if (expression.kind !== "new")
137085
- throw new Error("File @settings arguments are invalid.");
137086
- let templateId = null;
137087
- let name = null;
137088
- expression.args.forEach((argument2, index) => {
137089
- const argumentName = expression.argumentNames?.[index];
137090
- if (!argumentName)
137091
- throw new Error("File @settings arguments must be named.");
137092
- if (argumentName === "template") {
137093
- templateId = resolveTemplate2(argument2, templateIds);
137094
- return;
137095
- }
137096
- if (argumentName === "name") {
137097
- if (argument2.kind !== "litString")
137098
- throw new Error("File @settings name must be a string literal.");
137099
- name = argument2.value;
137100
- return;
137101
- }
137102
- throw new Error(
137103
- `Unknown project file @settings option ${argumentName}; expected template or name.`
137104
- );
137105
- });
137106
- return { templateId, name };
137107
- }
137108
- function resolveTemplate2(expression, templates) {
137109
- if (expression.kind === "litNull") return null;
137110
- if (expression.kind === "ident") {
137111
- const id2 = templates.get(expression.name);
137112
- if (!id2)
137113
- throw new Error(`Unknown file import template ${expression.name}.`);
137114
- return id2;
137115
- }
137116
- if (expression.kind === "call" && expression.callee.kind === "ident" && expression.callee.name === "Reference") {
137117
- const index = expression.argumentNames?.findIndex((name) => name === "id") ?? -1;
137118
- const id2 = expression.args[index >= 0 ? index : 0];
137119
- if (id2?.kind === "litString") return id2.value;
137120
- }
137121
- throw new Error(
137122
- "File template must be a template symbol, Reference(id: ...), or null."
137123
- );
137124
- }
137125
- function objectInitializerBody2(initializer, typeName) {
137126
- const open = initializer.indexOf("{");
137127
- const close = initializer.lastIndexOf("}");
137128
- if (!/^new\s*\(\s*\)/.test(initializer.trim()) || open < 0 || close < open) {
137129
- throw new Error(`${typeName} must use new() { ... }.`);
137130
- }
137131
- return initializer.slice(open + 1, close);
137132
- }
137133
137013
  function optionalString3(value) {
137134
137014
  return typeof value === "string" ? value : null;
137135
137015
  }
@@ -137178,25 +137058,18 @@ function visitBinaryFiles(directory, visit) {
137178
137058
  visit(path);
137179
137059
  }
137180
137060
  }
137181
- function safeFileName2(value) {
137182
- const fileName2 = basename(value).replace(/[\\/]/g, "-");
137183
- if (fileName2 === "." || fileName2 === ".." || fileName2.length === 0)
137184
- throw new Error(`Invalid project filename ${JSON.stringify(value)}.`);
137185
- return fileName2;
137186
- }
137187
137061
  function safePathSegment2(value) {
137188
137062
  return value.replace(/[^A-Za-z0-9._-]/g, "_") || "file";
137189
137063
  }
137190
137064
  function normalizeSlash2(value) {
137191
137065
  return value.split(sep2).join("/").replaceAll("\\", "/");
137192
137066
  }
137193
- var SUPPORTED_BINARY_TYPES, REGISTRY_ENTRY2;
137067
+ var SUPPORTED_BINARY_TYPES;
137194
137068
  var init_project_files = __esm({
137195
137069
  "src/project-source/project-files.ts"() {
137196
137070
  "use strict";
137197
137071
  init_project_file_source();
137198
137072
  init_workspace_source_path();
137199
- init_executable_parser();
137200
137073
  init_value_comparison();
137201
137074
  init_source_format();
137202
137075
  init_project_file_source();
@@ -137210,7 +137083,6 @@ var init_project_files = __esm({
137210
137083
  [".mp3", { kind: "audio", mimeType: "audio/mpeg" }],
137211
137084
  [".ogg", { kind: "audio", mimeType: "audio/ogg" }]
137212
137085
  ]);
137213
- REGISTRY_ENTRY2 = /(?:@id\(\s*("(?:[^"\\]|\\.)*")\s*\)\s*)?(?:@settings\(([\s\S]*?)\)\s*)?(NeoImage|NeoAudioClip)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*new\(\s*("(?:[^"\\]|\\.)*")\s*\)\s*;/gy;
137214
137086
  }
137215
137087
  });
137216
137088
 
@@ -140008,7 +139880,7 @@ async function pullProjectBinariesV4(args) {
140008
139880
  const previous = args.previousRecords[key];
140009
139881
  const localStatus = localById.get(record6.recordId);
140010
139882
  const declarationPresent = args.destructive === true || args.localBinaries === void 0 || previous === void 0 || localStatus !== void 0;
140011
- const path = localStatus?.path ?? previous?.projectBinary?.path ?? canonicalProjectBinaryPathV42(record6.data);
139883
+ const path = localStatus?.path ?? previous?.projectBinary?.path ?? canonicalProjectBinaryPathV4(record6.data);
140012
139884
  const absolute = resolvePath(path);
140013
139885
  const localDigest = existsSync9(absolute) ? sha256File(absolute) : null;
140014
139886
  const baseDigest = previous?.projectBinary?.sha256 ?? readSha256(previous?.data) ?? null;
@@ -140130,7 +140002,7 @@ async function pullProjectBinariesV4(args) {
140130
140002
  if (previous.recordKind !== "project-file") continue;
140131
140003
  if (args.document.records.has(key)) continue;
140132
140004
  const localStatus = localById.get(previous.recordId);
140133
- const path = localStatus?.path ?? previous.projectBinary?.path ?? canonicalProjectBinaryPathV42(previous.data);
140005
+ const path = localStatus?.path ?? previous.projectBinary?.path ?? canonicalProjectBinaryPathV4(previous.data);
140134
140006
  const absolute = resolvePath(path);
140135
140007
  const localDigest = existsSync9(absolute) ? sha256File(absolute) : null;
140136
140008
  const baseDigest = previous.projectBinary?.sha256 ?? readSha256(previous.data) ?? null;
@@ -140241,13 +140113,14 @@ var init_project_file_pull = __esm({
140241
140113
  init_concurrency();
140242
140114
  init_http();
140243
140115
  init_value_comparison();
140116
+ init_project_file_source();
140244
140117
  init_project_files();
140245
140118
  DOWNLOAD_CONCURRENCY = 8;
140246
140119
  }
140247
140120
  });
140248
140121
 
140249
140122
  // src/project-source/project-file-push.ts
140250
- import { basename as basename2, join as join11 } from "node:path";
140123
+ import { join as join11 } from "node:path";
140251
140124
  import { readFileSync as readFileSync13 } from "node:fs";
140252
140125
  import { randomUUID as randomUUID4 } from "node:crypto";
140253
140126
  async function cleanupRejectedProjectFilePushesV4(args) {
@@ -140323,11 +140196,17 @@ function prepareProjectFilePushesV4(args) {
140323
140196
  if (mimeType === null) {
140324
140197
  throw new Error(`Project file ${binary.path} has no MIME type.`);
140325
140198
  }
140199
+ const name = change.nextData.name;
140200
+ if (typeof name !== "string") {
140201
+ throw new Error(
140202
+ `Project file ${recordId} has upload bytes but its source change has no name.`
140203
+ );
140204
+ }
140326
140205
  prepared.push({
140327
140206
  uploadToken: `${recordId}-${prepared.length}`,
140328
140207
  recordId,
140329
140208
  replaceFileId: binary.action === "upload" ? recordId : null,
140330
- name: basename2(binary.path),
140209
+ name,
140331
140210
  fileType: binary.kind,
140332
140211
  mimeType,
140333
140212
  byteLength: bytes.byteLength,
@@ -155834,12 +155713,12 @@ import {
155834
155713
  unlinkSync as unlinkSync2,
155835
155714
  writeFileSync as writeFileSync10
155836
155715
  } from "node:fs";
155837
- import { basename as basename3, dirname as dirname8, join as join12 } from "node:path";
155716
+ import { basename as basename2, dirname as dirname8, join as join12 } from "node:path";
155838
155717
  function writeUnityFile(path, content) {
155839
155718
  mkdirSync9(dirname8(path), { recursive: true });
155840
155719
  const temporary = join12(
155841
155720
  dirname8(path),
155842
- `.${basename3(path)}.${randomUUID5()}.tmp`
155721
+ `.${basename2(path)}.${randomUUID5()}.tmp`
155843
155722
  );
155844
155723
  try {
155845
155724
  writeFileSync10(temporary, content);
@@ -158921,7 +158800,11 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
158921
158800
  (id2) => rewritePlacementValueId(id2, replacements)
158922
158801
  )
158923
158802
  })),
158924
- localStatus.sourceSnapshot
158803
+ localStatus.sourceSnapshot,
158804
+ localStatus.binaryFiles?.map((binary) => ({
158805
+ fileId: rewriteAssignedString(binary.fileId, replacements),
158806
+ path: binary.path
158807
+ }))
158925
158808
  );
158926
158809
  } catch (error) {
158927
158810
  workspace.state = {
@@ -159199,7 +159082,7 @@ function rewriteFilesFromState(workspace, records2 = new Map(
159199
159082
  data: recordState.data
159200
159083
  }
159201
159084
  ])
159202
- ), preservedSourceFiles = /* @__PURE__ */ new Map(), recordFiles = /* @__PURE__ */ new Map(), localValues = workspace.state.localValues, sourceSnapshot) {
159085
+ ), preservedSourceFiles = /* @__PURE__ */ new Map(), recordFiles = /* @__PURE__ */ new Map(), localValues = workspace.state.localValues, sourceSnapshot, binaryFiles) {
159203
159086
  const result = emitProjectDocumentSourcesV4(
159204
159087
  candidateValueRecordsForEmissionV4(
159205
159088
  records2,
@@ -159209,7 +159092,7 @@ function rewriteFilesFromState(workspace, records2 = new Map(
159209
159092
  {
159210
159093
  recordFiles,
159211
159094
  localValues,
159212
- binaryPaths: workspaceBinaryPathsV4(workspace)
159095
+ binaryPaths: workspaceBinaryPathsV4(workspace, binaryFiles)
159213
159096
  }
159214
159097
  );
159215
159098
  workspace.state.localValues = localValues;
@@ -172920,7 +172803,7 @@ import {
172920
172803
  writeFileSync as writeFileSync15
172921
172804
  } from "node:fs";
172922
172805
  import {
172923
- basename as basename4,
172806
+ basename as basename3,
172924
172807
  dirname as dirname12,
172925
172808
  isAbsolute as isAbsolute3,
172926
172809
  join as join20,
@@ -173364,7 +173247,7 @@ function selectedSpecPaths(workspace, selectors) {
173364
173247
  for (const selector of normalizedSelectors) {
173365
173248
  if (selector.includes("/") || /[*?]/u.test(selector)) continue;
173366
173249
  const basenameMatches = [...relativePaths.values()].filter(
173367
- (path) => basename4(path) === selector
173250
+ (path) => basename3(path) === selector
173368
173251
  );
173369
173252
  if (basenameMatches.length > 1) {
173370
173253
  throw new Error(
@@ -173373,7 +173256,7 @@ function selectedSpecPaths(workspace, selectors) {
173373
173256
  }
173374
173257
  }
173375
173258
  const configuredIncludes = workspace.config.test?.include ?? [];
173376
- const selectorMatches = (selector, path) => path === selector || basename4(path) === selector || path.startsWith(`${selector.replace(/\/$/u, "")}/`) || globMatches(selector, path);
173259
+ const selectorMatches = (selector, path) => path === selector || basename3(path) === selector || path.startsWith(`${selector.replace(/\/$/u, "")}/`) || globMatches(selector, path);
173377
173260
  const selectedPaths = all.filter((absolutePath) => {
173378
173261
  const path = relativePaths.get(absolutePath);
173379
173262
  const configured = configuredIncludes.length === 0 || configuredIncludes.some((pattern) => globMatches(pattern, path));
@@ -174135,14 +174018,14 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
174135
174018
  return fromProtected === "" || !fromProtected.startsWith(`..${sep9}`) && fromProtected !== ".." && !isAbsolute3(fromProtected);
174136
174019
  };
174137
174020
  for (const file of testBuildFiles(root)) {
174138
- if (!isProtected(file.path) && basename4(file.path).includes(".tmp-") && now - file.modifiedMs >= ABANDONED_TEMP_MAX_AGE_MS) {
174021
+ if (!isProtected(file.path) && basename3(file.path).includes(".tmp-") && now - file.modifiedMs >= ABANDONED_TEMP_MAX_AGE_MS) {
174139
174022
  rmSync10(file.path, { force: true });
174140
174023
  }
174141
174024
  }
174142
174025
  const files = testBuildFiles(root);
174143
174026
  let total = files.reduce((sum, file) => sum + file.size, 0);
174144
174027
  const removable = files.filter(
174145
- (file) => basename4(file.path) !== "manifest.json" && !isProtected(file.path)
174028
+ (file) => basename3(file.path) !== "manifest.json" && !isProtected(file.path)
174146
174029
  ).sort((left, right) => left.modifiedMs - right.modifiedMs);
174147
174030
  for (const file of removable) {
174148
174031
  if (total <= maxBytes) break;
@@ -180887,7 +180770,7 @@ __export(dev_exports, {
180887
180770
  runDev: () => runDev
180888
180771
  });
180889
180772
  import { watch } from "node:fs";
180890
- import { basename as basename5, dirname as dirname13 } from "node:path";
180773
+ import { basename as basename4, dirname as dirname13 } from "node:path";
180891
180774
  import { emitKeypressEvents } from "node:readline";
180892
180775
  async function runDev(loadWorkspace2, options) {
180893
180776
  const root = loadWorkspace2().root;
@@ -180924,7 +180807,7 @@ async function runDev(loadWorkspace2, options) {
180924
180807
  const watchConfigAsset = (path) => {
180925
180808
  if (configWatch?.path === path) return;
180926
180809
  configWatch?.watcher.close();
180927
- const name = basename5(path);
180810
+ const name = basename4(path);
180928
180811
  const watcher = watch(dirname13(path), (_event, filename) => {
180929
180812
  if (filename === name) schedule();
180930
180813
  });
@@ -181391,7 +181274,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
181391
181274
  async function main() {
181392
181275
  const args = parseArgs(process.argv.slice(2));
181393
181276
  if (args.command === "--version") {
181394
- console.log("0.70.0");
181277
+ console.log("0.70.1");
181395
181278
  return;
181396
181279
  }
181397
181280
  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.70.0",
3
+ "version": "0.70.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.70.0 -->
12
+ <!-- reviewed-through-cli: 0.70.1 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -237,7 +237,9 @@ Registry declarations and their image/audio paths may be in any project folder
237
237
  outside ignored/private directories. Update the registry path when moving a
238
238
  binary file. Renaming the registry symbol or replacing bytes retains the file
239
239
  ID when the declaration keeps its `@id`; keep the symbol aligned with the
240
- persisted file name, or set `@settings(name: ...)` explicitly.
240
+ persisted file name, or set `@settings(name: ...)` explicitly. The name must
241
+ end with the binary's extension, as in `@settings(name: "ClinkingCoin1.mp3")`,
242
+ because pull rebuilds the file's path from it.
241
243
  Omitting `@settings(template: ...)` uses the complete built-in Unity import
242
244
  defaults for that file kind.
243
245
 
@@ -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.70.0 -->
113
+ <!-- reviewed-through-cli: 0.70.1 -->
114
114
  ```
115
115
 
116
116
  The quoted version above is checked too, so this instruction cannot go stale
@@ -263,7 +263,8 @@ Registry symbols derive deterministically from the persisted file name. For
263
263
  example, `tree.cherryblossom.png` becomes `TreeCherryblossom`. Keep an authored
264
264
  declaration aligned with that spelling, or set an explicit persisted name such
265
265
  as `@settings(name: "TreeCherryBlossom.png")` when the binary path should stay
266
- different. With no template annotation, the CLI persists the complete built-in
266
+ different. The name must end with the binary's extension, because pull
267
+ rebuilds the file's path from it. With no template annotation, the CLI persists the complete built-in
267
268
  Unity defaults for the file kind.
268
269
 
269
270
  Pull/push compare server-verified SHA-256, not storage ETags. A divergent