@neocompose/cli 0.70.0 → 0.70.2

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.70.2] - 2026-10-06
4
+
5
+ - After `neo push` creates, replaces or moves a project file, its committed bytes become the file's merge base at its new path, as a pull's would. Before, the re-export after the push failed for every new file with "the pull recorded no bytes for it", and for a moved file with "missing". Either failure left the Unity project marked as a local export.
6
+
7
+ ## [0.70.1] - 2026-10-06
8
+
9
+ - `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".
10
+ - 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.
11
+ - 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.
12
+
3
13
  ## [0.70.0] - 2026-10-06
4
14
 
5
15
  - `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.2",
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;
@@ -140198,8 +140070,8 @@ async function download(data, fileId, client, workspace) {
140198
140070
  }
140199
140071
  return { bytes: response.bytes, sha256: computed };
140200
140072
  }
140201
- function committedProjectBinaryStateV4(previous, data) {
140202
- return isObjectRecord2(data) ? binaryState(previous.path, readSha256(data), data) : previous;
140073
+ function committedProjectBinaryStateV4(path, data) {
140074
+ return binaryState(path, readSha256(data), isObjectRecord2(data) ? data : {});
140203
140075
  }
140204
140076
  function binaryState(path, sha2562, data) {
140205
140077
  return {
@@ -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);
@@ -158903,6 +158782,18 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
158903
158782
  data: local.fullData
158904
158783
  });
158905
158784
  }
158785
+ for (const binary of localStatus.binaryFiles ?? []) {
158786
+ const key = recordStateKey(
158787
+ "project-file",
158788
+ rewriteAssignedString(binary.fileId, replacements)
158789
+ );
158790
+ const record6 = nextRecords[key];
158791
+ if (record6 === void 0 || !changedKeys.has(key)) continue;
158792
+ nextRecords[key] = {
158793
+ ...record6,
158794
+ projectBinary: committedProjectBinaryStateV4(binary.path, record6.data)
158795
+ };
158796
+ }
158906
158797
  const previousState = workspace.state;
158907
158798
  workspace.state = {
158908
158799
  ...previousState,
@@ -158921,7 +158812,11 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
158921
158812
  (id2) => rewritePlacementValueId(id2, replacements)
158922
158813
  )
158923
158814
  })),
158924
- localStatus.sourceSnapshot
158815
+ localStatus.sourceSnapshot,
158816
+ localStatus.binaryFiles?.map((binary) => ({
158817
+ fileId: rewriteAssignedString(binary.fileId, replacements),
158818
+ path: binary.path
158819
+ }))
158925
158820
  );
158926
158821
  } catch (error) {
158927
158822
  workspace.state = {
@@ -159131,14 +159026,13 @@ function foldChangedRecords(previous, changedRecords) {
159131
159026
  `Changed record "${key}" response is missing its snapshot content hash.`
159132
159027
  );
159133
159028
  }
159134
- const projectBinary = records2[key]?.projectBinary;
159135
159029
  records2[key] = {
159136
159030
  recordKind,
159137
159031
  recordId,
159138
159032
  contentHash: snapshot.contentHash,
159139
159033
  data: changed.data,
159140
159034
  file: records2[key]?.file,
159141
- projectBinary: projectBinary && committedProjectBinaryStateV4(projectBinary, changed.data)
159035
+ projectBinary: records2[key]?.projectBinary
159142
159036
  };
159143
159037
  }
159144
159038
  return records2;
@@ -159199,7 +159093,7 @@ function rewriteFilesFromState(workspace, records2 = new Map(
159199
159093
  data: recordState.data
159200
159094
  }
159201
159095
  ])
159202
- ), preservedSourceFiles = /* @__PURE__ */ new Map(), recordFiles = /* @__PURE__ */ new Map(), localValues = workspace.state.localValues, sourceSnapshot) {
159096
+ ), preservedSourceFiles = /* @__PURE__ */ new Map(), recordFiles = /* @__PURE__ */ new Map(), localValues = workspace.state.localValues, sourceSnapshot, binaryFiles) {
159203
159097
  const result = emitProjectDocumentSourcesV4(
159204
159098
  candidateValueRecordsForEmissionV4(
159205
159099
  records2,
@@ -159209,7 +159103,7 @@ function rewriteFilesFromState(workspace, records2 = new Map(
159209
159103
  {
159210
159104
  recordFiles,
159211
159105
  localValues,
159212
- binaryPaths: workspaceBinaryPathsV4(workspace)
159106
+ binaryPaths: workspaceBinaryPathsV4(workspace, binaryFiles)
159213
159107
  }
159214
159108
  );
159215
159109
  workspace.state.localValues = localValues;
@@ -172920,7 +172814,7 @@ import {
172920
172814
  writeFileSync as writeFileSync15
172921
172815
  } from "node:fs";
172922
172816
  import {
172923
- basename as basename4,
172817
+ basename as basename3,
172924
172818
  dirname as dirname12,
172925
172819
  isAbsolute as isAbsolute3,
172926
172820
  join as join20,
@@ -173364,7 +173258,7 @@ function selectedSpecPaths(workspace, selectors) {
173364
173258
  for (const selector of normalizedSelectors) {
173365
173259
  if (selector.includes("/") || /[*?]/u.test(selector)) continue;
173366
173260
  const basenameMatches = [...relativePaths.values()].filter(
173367
- (path) => basename4(path) === selector
173261
+ (path) => basename3(path) === selector
173368
173262
  );
173369
173263
  if (basenameMatches.length > 1) {
173370
173264
  throw new Error(
@@ -173373,7 +173267,7 @@ function selectedSpecPaths(workspace, selectors) {
173373
173267
  }
173374
173268
  }
173375
173269
  const configuredIncludes = workspace.config.test?.include ?? [];
173376
- const selectorMatches = (selector, path) => path === selector || basename4(path) === selector || path.startsWith(`${selector.replace(/\/$/u, "")}/`) || globMatches(selector, path);
173270
+ const selectorMatches = (selector, path) => path === selector || basename3(path) === selector || path.startsWith(`${selector.replace(/\/$/u, "")}/`) || globMatches(selector, path);
173377
173271
  const selectedPaths = all.filter((absolutePath) => {
173378
173272
  const path = relativePaths.get(absolutePath);
173379
173273
  const configured = configuredIncludes.length === 0 || configuredIncludes.some((pattern) => globMatches(pattern, path));
@@ -174135,14 +174029,14 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
174135
174029
  return fromProtected === "" || !fromProtected.startsWith(`..${sep9}`) && fromProtected !== ".." && !isAbsolute3(fromProtected);
174136
174030
  };
174137
174031
  for (const file of testBuildFiles(root)) {
174138
- if (!isProtected(file.path) && basename4(file.path).includes(".tmp-") && now - file.modifiedMs >= ABANDONED_TEMP_MAX_AGE_MS) {
174032
+ if (!isProtected(file.path) && basename3(file.path).includes(".tmp-") && now - file.modifiedMs >= ABANDONED_TEMP_MAX_AGE_MS) {
174139
174033
  rmSync10(file.path, { force: true });
174140
174034
  }
174141
174035
  }
174142
174036
  const files = testBuildFiles(root);
174143
174037
  let total = files.reduce((sum, file) => sum + file.size, 0);
174144
174038
  const removable = files.filter(
174145
- (file) => basename4(file.path) !== "manifest.json" && !isProtected(file.path)
174039
+ (file) => basename3(file.path) !== "manifest.json" && !isProtected(file.path)
174146
174040
  ).sort((left, right) => left.modifiedMs - right.modifiedMs);
174147
174041
  for (const file of removable) {
174148
174042
  if (total <= maxBytes) break;
@@ -180887,7 +180781,7 @@ __export(dev_exports, {
180887
180781
  runDev: () => runDev
180888
180782
  });
180889
180783
  import { watch } from "node:fs";
180890
- import { basename as basename5, dirname as dirname13 } from "node:path";
180784
+ import { basename as basename4, dirname as dirname13 } from "node:path";
180891
180785
  import { emitKeypressEvents } from "node:readline";
180892
180786
  async function runDev(loadWorkspace2, options) {
180893
180787
  const root = loadWorkspace2().root;
@@ -180924,7 +180818,7 @@ async function runDev(loadWorkspace2, options) {
180924
180818
  const watchConfigAsset = (path) => {
180925
180819
  if (configWatch?.path === path) return;
180926
180820
  configWatch?.watcher.close();
180927
- const name = basename5(path);
180821
+ const name = basename4(path);
180928
180822
  const watcher = watch(dirname13(path), (_event, filename) => {
180929
180823
  if (filename === name) schedule();
180930
180824
  });
@@ -181391,7 +181285,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
181391
181285
  async function main() {
181392
181286
  const args = parseArgs(process.argv.slice(2));
181393
181287
  if (args.command === "--version") {
181394
- console.log("0.70.0");
181288
+ console.log("0.70.2");
181395
181289
  return;
181396
181290
  }
181397
181291
  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.2",
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.2 -->
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.2 -->
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