@neocompose/cli 0.50.1 → 0.50.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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.50.2] - 2026-09-12
4
+
5
+ - Export Unity C# as separate files named after their generated types. Keep stable identities in the manifest to preserve Unity GUIDs through renames. Preserve unchanged files and their Unity metadata, remove obsolete owned outputs, and share the integrity manifest with editor synchronization.
6
+
3
7
  ## [0.50.1] - 2026-09-12
4
8
 
5
9
  - Reuse schema and symbol indexes while compiling a push, including migration bodies, and compile each changed member body once. Bulk scripted changes no longer rebuild the complete project for every body.
package/dist/neo.mjs CHANGED
@@ -35365,7 +35365,7 @@ var init_login = __esm({
35365
35365
  "project:localization:export",
35366
35366
  "project:localization:import",
35367
35367
  "project:release-channel:read",
35368
- // `neo export unity` writes project.json + NeoGeneratedTypes.cs headlessly
35368
+ // `neo export unity` writes project.json + generated C# files headlessly
35369
35369
  // (the escape hatch when game code references not-yet-generated members and
35370
35370
  // a broken compile blocks the in-editor sync).
35371
35371
  "unity:export",
@@ -132352,7 +132352,7 @@ var init_registry2 = __esm({
132352
132352
  "schema-contract/registry.mjs"() {
132353
132353
  "use strict";
132354
132354
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
132355
- cliVersion: "0.50.1",
132355
+ cliVersion: "0.50.2",
132356
132356
  projectFileUploadBatchSize: 32,
132357
132357
  documentRecords: {
132358
132358
  member: {
@@ -138828,13 +138828,177 @@ var init_dialogue_dryrun = __esm({
138828
138828
  }
138829
138829
  });
138830
138830
 
138831
+ // src/unity-generated-files.ts
138832
+ import { createHash as createHash12 } from "node:crypto";
138833
+ import {
138834
+ existsSync as existsSync16,
138835
+ mkdirSync as mkdirSync13,
138836
+ readFileSync as readFileSync21,
138837
+ unlinkSync,
138838
+ writeFileSync as writeFileSync13
138839
+ } from "node:fs";
138840
+ import { dirname as dirname11, join as join18 } from "node:path";
138841
+ function validatePath(path) {
138842
+ if (!/^Generated\/[a-zA-Z0-9_%./-]+\.g\.cs$/.test(path))
138843
+ throw new Error(`Invalid generated file path: ${path}`);
138844
+ if (path.split("/").some((part) => part === "." || part === ".." || part === ""))
138845
+ throw new Error(`Invalid generated file path segment: ${path}`);
138846
+ }
138847
+ function readManifest(path) {
138848
+ if (!existsSync16(path)) return null;
138849
+ const data = JSON.parse(readFileSync21(path, "utf8"));
138850
+ if (data === null || typeof data !== "object" || !("schemaVersion" in data) || data.schemaVersion !== 1)
138851
+ throw new Error(`Invalid generated-file manifest version: ${path}`);
138852
+ if (!("files" in data) || !Array.isArray(data.files) || data.files.length === 0)
138853
+ throw new Error(`Invalid generated-file manifest entries: ${path}`);
138854
+ if (!("projectId" in data) || typeof data.projectId !== "string")
138855
+ throw new Error(`Invalid generated-file manifest project: ${path}`);
138856
+ const ids = /* @__PURE__ */ new Set();
138857
+ const paths = /* @__PURE__ */ new Set();
138858
+ const files = data.files.map((entry) => {
138859
+ if (entry === null || typeof entry !== "object" || !("path" in entry) || typeof entry.path !== "string")
138860
+ throw new Error(`Invalid generated-file manifest entry: ${path}`);
138861
+ if (!("id" in entry) || typeof entry.id !== "string" || entry.id.length === 0)
138862
+ throw new Error(`Invalid generated-file manifest identity: ${path}`);
138863
+ if (ids.has(entry.id))
138864
+ throw new Error(
138865
+ `Duplicate generated-file manifest identity: ${entry.id}`
138866
+ );
138867
+ ids.add(entry.id);
138868
+ validatePath(entry.path);
138869
+ if (paths.has(entry.path.toLowerCase()))
138870
+ throw new Error(`Duplicate generated-file manifest path: ${entry.path}`);
138871
+ paths.add(entry.path.toLowerCase());
138872
+ return { id: entry.id, path: entry.path };
138873
+ });
138874
+ return { projectId: data.projectId, files };
138875
+ }
138876
+ function writeUnityGeneratedFiles(directory, projectId, files) {
138877
+ if (!Array.isArray(files) || files.length === 0)
138878
+ throw new Error(
138879
+ "The export returned no generated files. Update the server before exporting Unity code."
138880
+ );
138881
+ const expected = /* @__PURE__ */ new Set();
138882
+ const ids = /* @__PURE__ */ new Set();
138883
+ for (const file of files) {
138884
+ validatePath(file.path);
138885
+ if (typeof file.id !== "string" || file.id.length === 0)
138886
+ throw new Error(`Missing generated file identity: ${file.path}`);
138887
+ if (ids.has(file.id))
138888
+ throw new Error(`Duplicate generated file identity: ${file.id}`);
138889
+ ids.add(file.id);
138890
+ const key = file.path.toLowerCase();
138891
+ if (expected.has(key))
138892
+ throw new Error(`Duplicate generated file path: ${file.path}`);
138893
+ expected.add(key);
138894
+ if (typeof file.content !== "string" || file.content.trim().length === 0)
138895
+ throw new Error(`Empty generated C# file: ${file.path}`);
138896
+ }
138897
+ const manifestPath = join18(directory, "NeoGeneratedFiles.json");
138898
+ const manifest = readManifest(manifestPath);
138899
+ const previousFiles = manifest?.files ?? [];
138900
+ const previousById = new Map(previousFiles.map((file) => [file.id, file]));
138901
+ const previousPaths = new Set(
138902
+ previousFiles.map((file) => file.path.toLowerCase())
138903
+ );
138904
+ const incomingById = new Map(files.map((file) => [file.id, file]));
138905
+ const operations = [];
138906
+ for (const file of previousFiles) {
138907
+ if (incomingById.get(file.id)?.path === file.path && manifest?.projectId === projectId)
138908
+ continue;
138909
+ operations.push(
138910
+ [join18(directory, file.path), null],
138911
+ [join18(directory, file.path + ".meta"), null]
138912
+ );
138913
+ }
138914
+ operations.push(
138915
+ [join18(directory, "NeoGeneratedTypes.cs"), null],
138916
+ [join18(directory, "NeoGeneratedTypes.cs.meta"), null]
138917
+ );
138918
+ for (const file of files) {
138919
+ const old = manifest?.projectId === projectId ? previousById.get(file.id) : void 0;
138920
+ const path = join18(directory, file.path);
138921
+ if (old !== void 0 && old.path !== file.path) {
138922
+ if (!previousPaths.has(file.path.toLowerCase()) && (existsSync16(path) || existsSync16(path + ".meta")))
138923
+ throw new Error(
138924
+ `Generated rename destination is not owned by the manifest: ${file.path}`
138925
+ );
138926
+ const meta = join18(directory, old.path + ".meta");
138927
+ operations.push([
138928
+ path + ".meta",
138929
+ existsSync16(meta) ? readFileSync21(meta, "utf8") : null
138930
+ ]);
138931
+ }
138932
+ operations.push([path, file.content]);
138933
+ }
138934
+ operations.push([
138935
+ manifestPath,
138936
+ JSON.stringify(
138937
+ {
138938
+ schemaVersion: 1,
138939
+ projectId,
138940
+ files: [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((file) => ({
138941
+ id: file.id,
138942
+ path: file.path,
138943
+ hash: createHash12("sha256").update(file.content).digest("hex")
138944
+ }))
138945
+ },
138946
+ null,
138947
+ 2
138948
+ ) + "\n"
138949
+ ]);
138950
+ const previous = /* @__PURE__ */ new Map();
138951
+ for (const [path] of operations) {
138952
+ const key = path.toLowerCase();
138953
+ if (!previous.has(key))
138954
+ previous.set(key, {
138955
+ path,
138956
+ content: existsSync16(path) ? readFileSync21(path, "utf8") : null
138957
+ });
138958
+ }
138959
+ const write = (path, content) => {
138960
+ if (content === null) {
138961
+ if (existsSync16(path)) unlinkSync(path);
138962
+ } else {
138963
+ mkdirSync13(dirname11(path), { recursive: true });
138964
+ writeFileSync13(path, content);
138965
+ }
138966
+ };
138967
+ const changed = [];
138968
+ try {
138969
+ for (const [path, content] of operations) {
138970
+ const current = existsSync16(path) ? readFileSync21(path, "utf8") : null;
138971
+ if (current === content) continue;
138972
+ changed.push(path);
138973
+ write(path, content);
138974
+ }
138975
+ } catch (error) {
138976
+ const restored = /* @__PURE__ */ new Set();
138977
+ for (const path of changed.reverse()) {
138978
+ const key = path.toLowerCase();
138979
+ if (restored.has(key)) continue;
138980
+ restored.add(key);
138981
+ const original = previous.get(key);
138982
+ if (original.path !== path && existsSync16(path)) unlinkSync(path);
138983
+ const current = existsSync16(original.path) ? readFileSync21(original.path, "utf8") : null;
138984
+ if (current !== original.content) write(original.path, original.content);
138985
+ }
138986
+ throw error;
138987
+ }
138988
+ }
138989
+ var init_unity_generated_files = __esm({
138990
+ "src/unity-generated-files.ts"() {
138991
+ "use strict";
138992
+ }
138993
+ });
138994
+
138831
138995
  // src/commands/export.ts
138832
138996
  var export_exports = {};
138833
138997
  __export(export_exports, {
138834
138998
  runExportUnity: () => runExportUnity
138835
138999
  });
138836
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "node:fs";
138837
- import { join as join18 } from "node:path";
139000
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync14 } from "node:fs";
139001
+ import { join as join19 } from "node:path";
138838
139002
  async function runExportUnity(workspace, outDir) {
138839
139003
  if (outDir === null) {
138840
139004
  throw new Error(
@@ -138846,23 +139010,24 @@ async function runExportUnity(workspace, outDir) {
138846
139010
  `/api/projects/${workspace.config.projectId}/export`,
138847
139011
  { versionId: workspace.config.versionId }
138848
139012
  );
138849
- const resourcesDir = join18(outDir, "Resources", "Neo");
138850
- const localizationDir = join18(resourcesDir, "Localization");
138851
- const scriptsDir = join18(outDir, "Scripts", "Neo");
138852
- mkdirSync13(localizationDir, { recursive: true });
138853
- mkdirSync13(scriptsDir, { recursive: true });
138854
- writeFileSync13(join18(resourcesDir, "project.json"), response.projectJson);
138855
- writeFileSync13(
138856
- join18(scriptsDir, "NeoGeneratedTypes.cs"),
138857
- response.generatedTypes
138858
- );
139013
+ const resourcesDir = join19(outDir, "Resources", "Neo");
139014
+ const localizationDir = join19(resourcesDir, "Localization");
139015
+ const scriptsDir = join19(outDir, "Scripts", "Neo");
139016
+ mkdirSync14(localizationDir, { recursive: true });
139017
+ mkdirSync14(scriptsDir, { recursive: true });
139018
+ writeUnityGeneratedFiles(
139019
+ scriptsDir,
139020
+ response.projectId,
139021
+ response.generatedFiles
139022
+ );
139023
+ writeFileSync14(join19(resourcesDir, "project.json"), response.projectJson);
138859
139024
  for (const file of response.localizationFiles ?? []) {
138860
- writeFileSync13(join18(localizationDir, file.fileName), file.content);
139025
+ writeFileSync14(join19(localizationDir, file.fileName), file.content);
138861
139026
  }
138862
- console.log(`wrote ${join18(resourcesDir, "project.json")}`);
138863
- console.log(`wrote ${join18(scriptsDir, "NeoGeneratedTypes.cs")}`);
139027
+ console.log(`wrote ${join19(resourcesDir, "project.json")}`);
139028
+ console.log(`synchronized generated files in ${scriptsDir}`);
138864
139029
  for (const file of response.localizationFiles ?? []) {
138865
- console.log(`wrote ${join18(localizationDir, file.fileName)}`);
139030
+ console.log(`wrote ${join19(localizationDir, file.fileName)}`);
138866
139031
  }
138867
139032
  const diagnostics = response.diagnostics ?? [];
138868
139033
  for (const diagnostic of diagnostics) {
@@ -138875,6 +139040,7 @@ async function runExportUnity(workspace, outDir) {
138875
139040
  var init_export = __esm({
138876
139041
  "src/commands/export.ts"() {
138877
139042
  "use strict";
139043
+ init_unity_generated_files();
138878
139044
  init_http();
138879
139045
  }
138880
139046
  });
@@ -138885,7 +139051,7 @@ __export(dev_exports, {
138885
139051
  runDev: () => runDev
138886
139052
  });
138887
139053
  import { watch } from "node:fs";
138888
- import { join as join19 } from "node:path";
139054
+ import { join as join20 } from "node:path";
138889
139055
  import { emitKeypressEvents } from "node:readline";
138890
139056
  import { ConvexClient } from "convex/browser";
138891
139057
  function isSchemaSignal(value) {
@@ -138995,7 +139161,7 @@ async function runDev(workspace, options) {
138995
139161
  };
138996
139162
  for (const dir of ["Classes", "Enums"]) {
138997
139163
  try {
138998
- watch(join19(workspace.root, dir), { persistent: true }, onFileChange);
139164
+ watch(join20(workspace.root, dir), { persistent: true }, onFileChange);
138999
139165
  } catch {
139000
139166
  }
139001
139167
  }
@@ -139049,8 +139215,8 @@ __export(resolve_exports, {
139049
139215
  resolveMarkers: () => resolveMarkers,
139050
139216
  runResolve: () => runResolve
139051
139217
  });
139052
- import { readFileSync as readFileSync21, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "node:fs";
139053
- import { join as join20, relative as relative11 } from "node:path";
139218
+ import { readFileSync as readFileSync22, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "node:fs";
139219
+ import { join as join21, relative as relative11 } from "node:path";
139054
139220
  function runResolve(workspace, side) {
139055
139221
  const marked = readMarkedProjectSourcesV4(workspace.root);
139056
139222
  assertRecordConflictsResolveTogetherV4(
@@ -139060,7 +139226,7 @@ function runResolve(workspace, side) {
139060
139226
  const resolvedRecords = adoptServerConflictBases(workspace);
139061
139227
  let resolvedFiles = 0;
139062
139228
  for (const file of marked) {
139063
- writeFileSync14(file.absolutePath, resolveMarkers(file.source, side), "utf8");
139229
+ writeFileSync15(file.absolutePath, resolveMarkers(file.source, side), "utf8");
139064
139230
  resolvedFiles += 1;
139065
139231
  }
139066
139232
  let resolvedBinaries = 0;
@@ -139068,12 +139234,12 @@ function runResolve(workspace, side) {
139068
139234
  const binary = state.projectBinary;
139069
139235
  const conflict2 = binary?.conflict;
139070
139236
  if (binary === void 0 || conflict2 === void 0) continue;
139071
- const destination = join20(workspace.root, binary.path);
139237
+ const destination = join21(workspace.root, binary.path);
139072
139238
  if (side === "theirs") {
139073
139239
  if (conflict2.remoteSha256 !== null && conflict2.artifactPath !== void 0) {
139074
139240
  writeVerifiedBinaryDownloadV4(
139075
139241
  destination,
139076
- readFileSync21(join20(workspace.root, conflict2.artifactPath)),
139242
+ readFileSync22(join21(workspace.root, conflict2.artifactPath)),
139077
139243
  conflict2.remoteSha256
139078
139244
  );
139079
139245
  binary.sha256 = conflict2.remoteSha256;
@@ -139083,7 +139249,7 @@ function runResolve(workspace, side) {
139083
139249
  }
139084
139250
  }
139085
139251
  if (conflict2.artifactPath !== void 0) {
139086
- rmSync8(join20(workspace.root, conflict2.artifactPath), { force: true });
139252
+ rmSync8(join21(workspace.root, conflict2.artifactPath), { force: true });
139087
139253
  }
139088
139254
  delete binary.conflict;
139089
139255
  resolvedBinaries += 1;
@@ -139102,7 +139268,7 @@ function runResolve(workspace, side) {
139102
139268
  function readMarkedProjectSourcesV4(root) {
139103
139269
  const files = [];
139104
139270
  for (const absolutePath of listProjectSourceFilesV4(root)) {
139105
- const source = readFileSync21(absolutePath, "utf8");
139271
+ const source = readFileSync22(absolutePath, "utf8");
139106
139272
  if (detectConflictMarkers(source) === null) continue;
139107
139273
  files.push({
139108
139274
  path: normalizeWorkspaceSourcePath(relative11(root, absolutePath)),
@@ -139402,7 +139568,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
139402
139568
  async function main() {
139403
139569
  const args = parseArgs(process.argv.slice(2));
139404
139570
  if (args.command === "--version") {
139405
- console.log("0.50.1");
139571
+ console.log("0.50.2");
139406
139572
  return;
139407
139573
  }
139408
139574
  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.50.1",
3
+ "version": "0.50.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.50.1 -->
12
+ <!-- reviewed-through-cli: 0.50.2 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -88,7 +88,7 @@ wrappers.
88
88
  The marker near the top of `SKILL.md` must exactly match the package version:
89
89
 
90
90
  ```html
91
- <!-- reviewed-through-cli: 0.50.1 -->
91
+ <!-- reviewed-through-cli: 0.50.2 -->
92
92
  ```
93
93
 
94
94
  The quoted version above is checked too, so this instruction cannot go stale
@@ -246,3 +246,11 @@ honor both variables.
246
246
 
247
247
  Pass explicit project/version IDs and flags in automation. Prefer `--json` for
248
248
  machine-readable output and do not depend on interactive pickers or confirms.
249
+
250
+ ## Unity code export
251
+
252
+ `neo export unity --out <UnityAssetsDir>` writes separate C# files under
253
+ `Scripts/Neo/Generated/`, named after the generated C# types, plus project and localization JSON. It compares
254
+ contents before writing C#, preserves unchanged `.meta` files, removes obsolete
255
+ outputs listed in `NeoGeneratedFiles.json`, and removes the former
256
+ `NeoGeneratedTypes.cs`. Stable identities in the manifest preserve `.meta` GUIDs when types are renamed. Keep the manifest with the generated files.