@neocompose/cli 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/dist/neo.mjs +2284 -243
  2. package/package.json +1 -1
  3. package/skill/SKILL.md +0 -147
package/dist/neo.mjs CHANGED
@@ -278,7 +278,40 @@ var init_project_db_response_types = __esm({
278
278
  }
279
279
  });
280
280
 
281
+ // ../src/models/attributes/attribute-storage.ts
282
+ function isAttributeStorage(value) {
283
+ if (typeof value !== "string") return false;
284
+ return ATTRIBUTE_STORAGE_VALUES.has(value);
285
+ }
286
+ function isEffectiveAttributeStorage(value) {
287
+ if (!isAttributeStorage(value)) return false;
288
+ return value !== "inherit" /* Inherit */;
289
+ }
290
+ var AttributeStorage, ATTRIBUTE_STORAGE_VALUES;
291
+ var init_attribute_storage = __esm({
292
+ "../src/models/attributes/attribute-storage.ts"() {
293
+ "use strict";
294
+ AttributeStorage = /* @__PURE__ */ ((AttributeStorage3) => {
295
+ AttributeStorage3["Inherit"] = "inherit";
296
+ AttributeStorage3["Static"] = "static";
297
+ AttributeStorage3["Save"] = "save";
298
+ AttributeStorage3["Session"] = "session";
299
+ return AttributeStorage3;
300
+ })(AttributeStorage || {});
301
+ ATTRIBUTE_STORAGE_VALUES = new Set(
302
+ Object.values(AttributeStorage)
303
+ );
304
+ }
305
+ });
306
+
281
307
  // ../src/models/project/project-root-attributes.ts
308
+ function buildRootStorageMap(project) {
309
+ const map = /* @__PURE__ */ new Map();
310
+ for (const root of PROJECT_ROOTS) {
311
+ map.set(project[root.projectField], root.storage);
312
+ }
313
+ return map;
314
+ }
282
315
  function projectRootAttributesInDisplayOrder(project) {
283
316
  return PROJECT_ROOTS.map((root) => ({
284
317
  root,
@@ -289,27 +322,28 @@ var PROJECT_ROOTS;
289
322
  var init_project_root_attributes = __esm({
290
323
  "../src/models/project/project-root-attributes.ts"() {
291
324
  "use strict";
325
+ init_attribute_storage();
292
326
  PROJECT_ROOTS = [
293
327
  {
294
328
  key: "Assets",
295
329
  choice: "assets",
296
330
  projectField: "rootAssetsAttributeId",
297
331
  label: "Assets",
298
- writable: false
332
+ storage: "static" /* Static */
299
333
  },
300
334
  {
301
335
  key: "Save",
302
336
  choice: "save",
303
337
  projectField: "rootSaveFileAttributeId",
304
338
  label: "Save",
305
- writable: true
339
+ storage: "save" /* Save */
306
340
  },
307
341
  {
308
342
  key: "Session",
309
343
  choice: "session",
310
344
  projectField: "rootSessionAttributeId",
311
345
  label: "Session",
312
- writable: true
346
+ storage: "session" /* Session */
313
347
  }
314
348
  ];
315
349
  }
@@ -516,6 +550,29 @@ function readWorkspaceConfig(root) {
516
550
  `"${NEO_CONFIG_FILE}" is missing string field "apiBaseUrl".`
517
551
  );
518
552
  }
553
+ const profile = config.profile === "release" ? "release" : "editor";
554
+ const convexUrl = typeof config.convexUrl === "string" ? config.convexUrl : void 0;
555
+ if (typeof config.unityConfigPath === "string") {
556
+ if (config.projectId !== void 0) {
557
+ throw new Error(
558
+ `"${NEO_CONFIG_FILE}" sets "unityConfigPath", so "projectId" must be removed \u2014 the Unity config asset is the single source of truth.`
559
+ );
560
+ }
561
+ if (config.versionId !== void 0) {
562
+ throw new Error(
563
+ `"${NEO_CONFIG_FILE}" sets "unityConfigPath", so "versionId" must be removed \u2014 the Unity config asset is the single source of truth.`
564
+ );
565
+ }
566
+ const ids = readUnityConfigIds(root, config.unityConfigPath);
567
+ return {
568
+ apiBaseUrl: config.apiBaseUrl,
569
+ projectId: ids.projectId,
570
+ versionId: ids.versionId,
571
+ profile,
572
+ convexUrl,
573
+ unityConfigPath: config.unityConfigPath
574
+ };
575
+ }
519
576
  if (typeof config.projectId !== "string") {
520
577
  throw new Error(
521
578
  `"${NEO_CONFIG_FILE}" is missing string field "projectId".`
@@ -526,23 +583,79 @@ function readWorkspaceConfig(root) {
526
583
  `"${NEO_CONFIG_FILE}" is missing string field "versionId".`
527
584
  );
528
585
  }
529
- const profile = config.profile === "release" ? "release" : "editor";
530
586
  return {
531
587
  apiBaseUrl: config.apiBaseUrl,
532
588
  projectId: config.projectId,
533
589
  versionId: config.versionId,
534
590
  profile,
535
- convexUrl: typeof config.convexUrl === "string" ? config.convexUrl : void 0
591
+ convexUrl
536
592
  };
537
593
  }
538
594
  function writeWorkspaceConfig(root, config) {
595
+ let persisted = { ...config };
596
+ if (config.unityConfigPath !== void 0) {
597
+ const { projectId, versionId, ...rest } = persisted;
598
+ void projectId;
599
+ void versionId;
600
+ persisted = rest;
601
+ writeUnityConfigVersionId(root, config.unityConfigPath, config.versionId);
602
+ }
539
603
  writeFileSync2(
540
604
  join2(root, NEO_CONFIG_FILE),
541
- `${JSON.stringify(config, null, 2)}
605
+ `${JSON.stringify(persisted, null, 2)}
542
606
  `,
543
607
  "utf8"
544
608
  );
545
609
  }
610
+ function readUnityConfigIds(root, unityConfigPath) {
611
+ const assetPath = resolve(root, unityConfigPath);
612
+ if (!existsSync2(assetPath)) {
613
+ throw new Error(
614
+ `"unityConfigPath" in "${NEO_CONFIG_FILE}" points to "${assetPath}", which does not exist.`
615
+ );
616
+ }
617
+ const content = readFileSync2(assetPath, "utf8");
618
+ const projectId = matchUnityScalarField(content, "projectId");
619
+ if (projectId === null) {
620
+ throw new Error(
621
+ `Unity config asset "${assetPath}" has no "projectId" field.`
622
+ );
623
+ }
624
+ const versionId = matchUnityScalarField(content, "versionId");
625
+ if (versionId === null) {
626
+ throw new Error(
627
+ `Unity config asset "${assetPath}" has no "versionId" field.`
628
+ );
629
+ }
630
+ return { projectId, versionId };
631
+ }
632
+ function writeUnityConfigVersionId(root, unityConfigPath, versionId) {
633
+ const assetPath = resolve(root, unityConfigPath);
634
+ if (!existsSync2(assetPath)) {
635
+ throw new Error(
636
+ `"unityConfigPath" in "${NEO_CONFIG_FILE}" points to "${assetPath}", which does not exist.`
637
+ );
638
+ }
639
+ const content = readFileSync2(assetPath, "utf8");
640
+ const current = matchUnityScalarField(content, "versionId");
641
+ if (current === null) {
642
+ throw new Error(
643
+ `Unity config asset "${assetPath}" has no "versionId" field to update.`
644
+ );
645
+ }
646
+ if (current === versionId) return;
647
+ const updated = content.replace(
648
+ /^(\s*versionId:[ \t]*).*$/m,
649
+ `$1${versionId}`
650
+ );
651
+ writeFileSync2(assetPath, updated, "utf8");
652
+ }
653
+ function matchUnityScalarField(content, field) {
654
+ const match = new RegExp(`^\\s*${field}:[ \\t]*(\\S+)[ \\t]*$`, "m").exec(
655
+ content
656
+ );
657
+ return match === null ? null : match[1];
658
+ }
546
659
  function readWorkspaceState(root) {
547
660
  const statePath = join2(root, NEO_STATE_DIR, NEO_STATE_FILE);
548
661
  if (!existsSync2(statePath)) {
@@ -713,6 +826,11 @@ function isNSTypeInfoAsset(value) {
713
826
  const t = value.type;
714
827
  return t === 11 /* Sprite */ || t === 12 /* Audio */;
715
828
  }
829
+ function isNSTypeInfoVector(value) {
830
+ if (!isNSTypeInfoBase(value)) return false;
831
+ const t = value.type;
832
+ return t === 14 /* Vector2 */ || t === 15 /* Vector2Int */ || t === 16 /* Vector3 */ || t === 17 /* Vector3Int */;
833
+ }
716
834
  function isNSTypeInfoUnknown(value) {
717
835
  const v = value;
718
836
  return typeof v === "object" && v !== null && v.type === NS_TYPE_UNKNOWN && typeof v.required === "boolean";
@@ -748,7 +866,7 @@ function isNSTypeInfoEnum(value) {
748
866
  return typeof v.enumId === "string" && v.enumId.length > 0;
749
867
  }
750
868
  function isNSTypeInfo(value) {
751
- return isNSTypeInfoUnknown(value) || isNSTypeInfoPrimitive(value) || isNSTypeInfoAsset(value) || isNSTypeInfoCustom(value) || isNSTypeInfoCollection(value) || isNSTypeInfoLookup(value) || isNSTypeInfoEnum(value);
869
+ return isNSTypeInfoUnknown(value) || isNSTypeInfoPrimitive(value) || isNSTypeInfoAsset(value) || isNSTypeInfoVector(value) || isNSTypeInfoCustom(value) || isNSTypeInfoCollection(value) || isNSTypeInfoLookup(value) || isNSTypeInfoEnum(value);
752
870
  }
753
871
  function isNSArgumentTypeInfo(value) {
754
872
  return isRequiredType(value, isNSArgumentTypeInfoUnsafe);
@@ -824,6 +942,59 @@ function isAttributeString(value) {
824
942
  function isAttributeFloatBase(value) {
825
943
  return asTypedAttributeBase(value, 4 /* Float */) !== null;
826
944
  }
945
+ function hasExactKeys(value, keys) {
946
+ const actual = Object.keys(value);
947
+ if (actual.length !== keys.length) return false;
948
+ return keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
949
+ }
950
+ function isFiniteNumberValue(value) {
951
+ return typeof value === "number" && Number.isFinite(value);
952
+ }
953
+ function isVectorComponentValue(value, integer) {
954
+ if (!isFiniteNumberValue(value)) return false;
955
+ if (!integer) return true;
956
+ return Number.isInteger(value);
957
+ }
958
+ function isVector2ValueWithComponents(value, integer) {
959
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
960
+ return false;
961
+ }
962
+ const record = value;
963
+ if (!hasExactKeys(record, ["x", "y"])) return false;
964
+ return isVectorComponentValue(record.x, integer) && isVectorComponentValue(record.y, integer);
965
+ }
966
+ function isVector3ValueWithComponents(value, integer) {
967
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
968
+ return false;
969
+ }
970
+ const record = value;
971
+ if (!hasExactKeys(record, ["x", "y", "z"])) return false;
972
+ return isVectorComponentValue(record.x, integer) && isVectorComponentValue(record.y, integer) && isVectorComponentValue(record.z, integer);
973
+ }
974
+ function isVector2Value(value) {
975
+ return isVector2ValueWithComponents(value, false);
976
+ }
977
+ function isVector2IntValue(value) {
978
+ return isVector2ValueWithComponents(value, true);
979
+ }
980
+ function isVector3Value(value) {
981
+ return isVector3ValueWithComponents(value, false);
982
+ }
983
+ function isVector3IntValue(value) {
984
+ return isVector3ValueWithComponents(value, true);
985
+ }
986
+ function isAttributeVector2Base(value) {
987
+ return asTypedAttributeBase(value, 14 /* Vector2 */) !== null;
988
+ }
989
+ function isAttributeVector2IntBase(value) {
990
+ return asTypedAttributeBase(value, 15 /* Vector2Int */) !== null;
991
+ }
992
+ function isAttributeVector3Base(value) {
993
+ return asTypedAttributeBase(value, 16 /* Vector3 */) !== null;
994
+ }
995
+ function isAttributeVector3IntBase(value) {
996
+ return asTypedAttributeBase(value, 17 /* Vector3Int */) !== null;
997
+ }
827
998
  function isAttributeDictionaryBase(value) {
828
999
  const v = asTypedAttributeBase(value, 5 /* Dictionary */);
829
1000
  if (v === null) return false;
@@ -931,13 +1102,25 @@ function isAttributeFunctionBase(value) {
931
1102
  }
932
1103
  return true;
933
1104
  }
1105
+ function attributeTypeSupportsStorage(type) {
1106
+ if (type === 10 /* NSGetter */) return false;
1107
+ return type !== 13 /* Function */;
1108
+ }
1109
+ function hasValidStorageField(v) {
1110
+ if (v.storage === void 0) return true;
1111
+ if (v.storage === null) return true;
1112
+ if (!isAttributeStorage(v.storage)) return false;
1113
+ if (typeof v.type !== "number") return false;
1114
+ return attributeTypeSupportsStorage(v.type);
1115
+ }
934
1116
  function isAttributeBase(value) {
935
1117
  const v = value;
936
- const matchesConcreteType = isAttributeNullBase(value) || isAttributeBoolBase(value) || isAttributeIntBase(value) || isAttributeStringBase(value) || isAttributeFloatBase(value) || isAttributeDictionaryBase(value) || isAttributeListBase(value) || isAttributeCustomBase(value) || isAttributeEnumBase(value) || isAttributeLookupBase(value) || isAttributeNSGetterBase(value) || isAttributeSpriteBase(value) || isAttributeAudioBase(value) || isAttributeFunctionBase(value);
1118
+ const matchesConcreteType = isAttributeNullBase(value) || isAttributeBoolBase(value) || isAttributeIntBase(value) || isAttributeStringBase(value) || isAttributeFloatBase(value) || isAttributeDictionaryBase(value) || isAttributeListBase(value) || isAttributeCustomBase(value) || isAttributeEnumBase(value) || isAttributeLookupBase(value) || isAttributeNSGetterBase(value) || isAttributeSpriteBase(value) || isAttributeAudioBase(value) || isAttributeFunctionBase(value) || isAttributeVector2Base(value) || isAttributeVector2IntBase(value) || isAttributeVector3Base(value) || isAttributeVector3IntBase(value);
937
1119
  if (!matchesConcreteType) return false;
938
1120
  if (typeof v?.name !== "string") return false;
939
1121
  if (typeof v.locked !== "boolean") return false;
940
1122
  if (typeof v.required !== "boolean") return false;
1123
+ if (!hasValidStorageField(v)) return false;
941
1124
  if (v.system !== void 0 && v.system !== null) {
942
1125
  return isSystemMetadata(v.system);
943
1126
  }
@@ -959,6 +1142,7 @@ var init_attribute_types = __esm({
959
1142
  "use strict";
960
1143
  init_core();
961
1144
  init_neoscript();
1145
+ init_attribute_storage();
962
1146
  AttributeType = /* @__PURE__ */ ((AttributeType9) => {
963
1147
  AttributeType9[AttributeType9["Null"] = 0] = "Null";
964
1148
  AttributeType9[AttributeType9["Bool"] = 1] = "Bool";
@@ -974,6 +1158,10 @@ var init_attribute_types = __esm({
974
1158
  AttributeType9[AttributeType9["Sprite"] = 11] = "Sprite";
975
1159
  AttributeType9[AttributeType9["Audio"] = 12] = "Audio";
976
1160
  AttributeType9[AttributeType9["Function"] = 13] = "Function";
1161
+ AttributeType9[AttributeType9["Vector2"] = 14] = "Vector2";
1162
+ AttributeType9[AttributeType9["Vector2Int"] = 15] = "Vector2Int";
1163
+ AttributeType9[AttributeType9["Vector3"] = 16] = "Vector3";
1164
+ AttributeType9[AttributeType9["Vector3Int"] = 17] = "Vector3Int";
977
1165
  return AttributeType9;
978
1166
  })(AttributeType || {});
979
1167
  FunctionAttributeNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -1010,7 +1198,7 @@ var init_attribute_types_db_response = __esm({
1010
1198
  // ../src/models/custom-types/custom-types.ts
1011
1199
  function isCustomTypeBase(value) {
1012
1200
  const v = value;
1013
- return typeof v?.name === "string" && isCustomTypeSchema(v?.schema) && isOptionalSchemaKeyOrder(v?.schemaKeyOrder) && typeof v?.hiddenInAttributeSelector === "boolean" && typeof v?.isAbstract === "boolean" && (v.system === void 0 || v.system === null || isSystemMetadata(v.system));
1201
+ return typeof v?.name === "string" && isCustomTypeSchema(v?.schema) && isOptionalSchemaKeyOrder(v?.schemaKeyOrder) && typeof v?.hiddenInAttributeSelector === "boolean" && typeof v?.isAbstract === "boolean" && (v.allowedStorage === void 0 || v.allowedStorage === null || isEffectiveAttributeStorage(v.allowedStorage)) && (v.system === void 0 || v.system === null || isSystemMetadata(v.system));
1014
1202
  }
1015
1203
  function getCustomTypeSchemaKeyOrder(type) {
1016
1204
  const schemaKeys = Object.keys(type.schema);
@@ -1046,6 +1234,7 @@ var init_custom_types = __esm({
1046
1234
  "../src/models/custom-types/custom-types.ts"() {
1047
1235
  "use strict";
1048
1236
  init_core();
1237
+ init_attribute_storage();
1049
1238
  }
1050
1239
  });
1051
1240
 
@@ -1645,6 +1834,165 @@ var init_build_default_attribute_value = __esm({
1645
1834
  }
1646
1835
  });
1647
1836
 
1837
+ // ../src/models/attributes/effective-storage.ts
1838
+ function declaredAttributeStorage(attributeId, attributes) {
1839
+ const declared = walkExtendsAttributeChain(attributeId, attributes, (a) => {
1840
+ const storage = a.storage;
1841
+ return isAttributeStorage(storage) ? storage : void 0;
1842
+ });
1843
+ return declared ?? "inherit" /* Inherit */;
1844
+ }
1845
+ var EffectiveStorageResolver;
1846
+ var init_effective_storage = __esm({
1847
+ "../src/models/attributes/effective-storage.ts"() {
1848
+ "use strict";
1849
+ init_attribute_storage();
1850
+ init_attribute_types();
1851
+ init_structural_narrow();
1852
+ init_custom_types();
1853
+ init_inheritance();
1854
+ EffectiveStorageResolver = class {
1855
+ constructor(ctx) {
1856
+ this.ctx = ctx;
1857
+ for (const attribute of ctx.attributes) {
1858
+ this.attributeById.set(attribute.id, attribute);
1859
+ }
1860
+ this.buildParentEdges();
1861
+ }
1862
+ ctx;
1863
+ attributeById = /* @__PURE__ */ new Map();
1864
+ parentIdsByAttributeId = /* @__PURE__ */ new Map();
1865
+ mergedSchemaByTypeId = /* @__PURE__ */ new Map();
1866
+ effectiveByAttributeId = /* @__PURE__ */ new Map();
1867
+ resolving = /* @__PURE__ */ new Set();
1868
+ /** Declared storage of `attributeId`, resolved through the extends chain. */
1869
+ declaredStorage(attributeId) {
1870
+ return declaredAttributeStorage(attributeId, this.ctx.attributes);
1871
+ }
1872
+ /** Placement-parent attribute ids of `attributeId` (empty for roots). */
1873
+ parentAttributeIds(attributeId) {
1874
+ if (this.ctx.rootStorage.has(attributeId)) return [];
1875
+ return this.parentIdsByAttributeId.get(attributeId) ?? [];
1876
+ }
1877
+ /**
1878
+ * The set of effective storages of `attributeId` across every placement
1879
+ * (specs/attribute-storage.md §2.3, generalized to the placement *graph*).
1880
+ */
1881
+ effectiveStorages(attributeId) {
1882
+ const cached = this.effectiveByAttributeId.get(attributeId);
1883
+ if (cached !== void 0) return cached;
1884
+ if (this.resolving.has(attributeId)) {
1885
+ return /* @__PURE__ */ new Set();
1886
+ }
1887
+ this.resolving.add(attributeId);
1888
+ try {
1889
+ const result = /* @__PURE__ */ new Set();
1890
+ const rootStorage = this.ctx.rootStorage.get(attributeId);
1891
+ if (rootStorage !== void 0) {
1892
+ result.add(rootStorage);
1893
+ } else {
1894
+ const declared = this.declaredStorage(attributeId);
1895
+ if (declared !== "inherit" /* Inherit */) {
1896
+ result.add(declared);
1897
+ } else {
1898
+ for (const parentId of this.parentAttributeIds(attributeId)) {
1899
+ for (const storage of this.effectiveStorages(parentId)) {
1900
+ result.add(storage);
1901
+ }
1902
+ }
1903
+ }
1904
+ }
1905
+ this.effectiveByAttributeId.set(attributeId, result);
1906
+ return result;
1907
+ } finally {
1908
+ this.resolving.delete(attributeId);
1909
+ }
1910
+ }
1911
+ /**
1912
+ * Effective storage for one concrete placement chain, ordered root-first
1913
+ * (the project root at index 0, the target attribute last). Used when the
1914
+ * caller knows the exact path (NeoScript member walks, value trees).
1915
+ */
1916
+ effectiveStorageForPath(attributeIdsRootToLeaf) {
1917
+ for (let i = attributeIdsRootToLeaf.length - 1; i >= 0; i--) {
1918
+ const attributeId = attributeIdsRootToLeaf[i];
1919
+ const rootStorage = this.ctx.rootStorage.get(attributeId);
1920
+ if (rootStorage !== void 0 && i === 0) return rootStorage;
1921
+ const declared = this.declaredStorage(attributeId);
1922
+ if (declared !== "inherit" /* Inherit */) return declared;
1923
+ }
1924
+ const rootId = attributeIdsRootToLeaf[0];
1925
+ const anchored = rootId === void 0 ? void 0 : this.ctx.rootStorage.get(rootId);
1926
+ if (anchored !== void 0) return anchored;
1927
+ throw new Error(
1928
+ `Storage path did not resolve: no attribute in the chain declares a storage class and the chain head "${rootId ?? "(empty)"}" is not a stamped project root.`
1929
+ );
1930
+ }
1931
+ /** Custom type's merged (inheritance-resolved) schema, memoized. */
1932
+ mergedSchema(typeId) {
1933
+ const cached = this.mergedSchemaByTypeId.get(typeId);
1934
+ if (cached !== void 0) return cached;
1935
+ const chain = resolveInheritanceChain(typeId, this.ctx.types);
1936
+ const merged = chain.length === 0 ? [] : mergeSchemas(chain);
1937
+ this.mergedSchemaByTypeId.set(typeId, merged);
1938
+ return merged;
1939
+ }
1940
+ /** Resolved per-type custom field — `customTypeId` through extends chain. */
1941
+ resolvedCustomTypeId(attributeId) {
1942
+ return walkExtendsAttributeChain(
1943
+ attributeId,
1944
+ this.ctx.attributes,
1945
+ (a) => getOptionalString(a, "customTypeId")
1946
+ );
1947
+ }
1948
+ /** Resolved `entryAttributeId` through the extends chain. */
1949
+ resolvedEntryAttributeId(attributeId) {
1950
+ return walkExtendsAttributeChain(
1951
+ attributeId,
1952
+ this.ctx.attributes,
1953
+ (a) => getOptionalString(a, "entryAttributeId")
1954
+ );
1955
+ }
1956
+ /** Resolved `collectionAttributeId` through the extends chain. */
1957
+ resolvedCollectionAttributeId(attributeId) {
1958
+ return walkExtendsAttributeChain(
1959
+ attributeId,
1960
+ this.ctx.attributes,
1961
+ (a) => getOptionalString(a, "collectionAttributeId")
1962
+ );
1963
+ }
1964
+ attribute(attributeId) {
1965
+ return this.attributeById.get(attributeId);
1966
+ }
1967
+ buildParentEdges() {
1968
+ const addEdge = (childId, parentId) => {
1969
+ const existing = this.parentIdsByAttributeId.get(childId);
1970
+ if (existing === void 0) {
1971
+ this.parentIdsByAttributeId.set(childId, [parentId]);
1972
+ return;
1973
+ }
1974
+ if (!existing.includes(parentId)) existing.push(parentId);
1975
+ };
1976
+ for (const attribute of this.ctx.attributes) {
1977
+ if (attribute.type === 7 /* Custom */) {
1978
+ const typeId = this.resolvedCustomTypeId(attribute.id);
1979
+ if (typeId !== void 0) {
1980
+ for (const entry of this.mergedSchema(typeId)) {
1981
+ addEdge(entry.attributeId, attribute.id);
1982
+ }
1983
+ }
1984
+ continue;
1985
+ }
1986
+ if (attribute.type === 6 /* List */ || attribute.type === 5 /* Dictionary */) {
1987
+ const entryId = this.resolvedEntryAttributeId(attribute.id);
1988
+ if (entryId !== void 0) addEdge(entryId, attribute.id);
1989
+ }
1990
+ }
1991
+ }
1992
+ };
1993
+ }
1994
+ });
1995
+
1648
1996
  // ../src/models/attributes/value-responses.ts
1649
1997
  var init_value_responses = __esm({
1650
1998
  "../src/models/attributes/value-responses.ts"() {
@@ -1678,9 +2026,11 @@ var init_delete_attribute_response = __esm({
1678
2026
  var init_attributes = __esm({
1679
2027
  "../src/models/attributes/index.ts"() {
1680
2028
  "use strict";
2029
+ init_attribute_storage();
1681
2030
  init_attribute_types();
1682
2031
  init_attribute_types_db_response();
1683
2032
  init_build_default_attribute_value();
2033
+ init_effective_storage();
1684
2034
  init_value_responses();
1685
2035
  init_edit_attribute_props();
1686
2036
  init_delete_attribute_response();
@@ -1747,6 +2097,7 @@ var init_localization_types = __esm({
1747
2097
  var init_localization_export = __esm({
1748
2098
  "../src/models/localization/localization-export.ts"() {
1749
2099
  "use strict";
2100
+ init_localization_types();
1750
2101
  }
1751
2102
  });
1752
2103
 
@@ -2102,7 +2453,11 @@ var init_projection = __esm({
2102
2453
  NSGetter: 10,
2103
2454
  Sprite: 11,
2104
2455
  Audio: 12,
2105
- Function: 13
2456
+ Function: 13,
2457
+ Vector2: 14,
2458
+ Vector2Int: 15,
2459
+ Vector3: 16,
2460
+ Vector3Int: 17
2106
2461
  };
2107
2462
  ATTRIBUTE_CLASS_BY_TYPE = {
2108
2463
  [ATTRIBUTE_TYPE.Null]: "NeoNull",
@@ -2118,7 +2473,11 @@ var init_projection = __esm({
2118
2473
  [ATTRIBUTE_TYPE.NSGetter]: "NeoGetter",
2119
2474
  [ATTRIBUTE_TYPE.Sprite]: "NeoSprite",
2120
2475
  [ATTRIBUTE_TYPE.Audio]: "NeoAudio",
2121
- [ATTRIBUTE_TYPE.Function]: "NeoFunction"
2476
+ [ATTRIBUTE_TYPE.Function]: "NeoFunction",
2477
+ [ATTRIBUTE_TYPE.Vector2]: "NeoVector2",
2478
+ [ATTRIBUTE_TYPE.Vector2Int]: "NeoVector2Int",
2479
+ [ATTRIBUTE_TYPE.Vector3]: "NeoVector3",
2480
+ [ATTRIBUTE_TYPE.Vector3Int]: "NeoVector3Int"
2122
2481
  };
2123
2482
  ATTRIBUTE_TYPE_BY_CLASS = new Map(
2124
2483
  Object.entries(ATTRIBUTE_CLASS_BY_TYPE).map(([type, name]) => [
@@ -2836,6 +3195,18 @@ function reconstructTypeFile(parsed, typeIdsByName, enumIdsByName, baseRecords,
2836
3195
  data.name = namedString(marker, "Name", parsed.file) ?? declaration.name;
2837
3196
  data.isAbstract = declaration.isAbstract;
2838
3197
  data.hiddenInAttributeSelector = namedBoolean(marker, "Hidden", parsed.file) ?? false;
3198
+ const allowedStorage = namedString(marker, "AllowedStorage", parsed.file);
3199
+ if (allowedStorage !== void 0) {
3200
+ if (allowedStorage !== "static" && allowedStorage !== "save" && allowedStorage !== "session") {
3201
+ throw new SchemaParseError(
3202
+ `AllowedStorage must be "static", "save", or "session"; found "${allowedStorage}".`,
3203
+ parsed.file,
3204
+ marker.line,
3205
+ marker.column
3206
+ );
3207
+ }
3208
+ data.allowedStorage = allowedStorage;
3209
+ }
2839
3210
  const extendsTypeIdArg = namedString(marker, "ExtendsTypeId", parsed.file);
2840
3211
  if (extendsTypeIdArg !== void 0) {
2841
3212
  data.extendsTypeId = extendsTypeIdArg;
@@ -2959,6 +3330,18 @@ function reconstructProperty(file, property, typeIdsByName, enumIdsByName, baseR
2959
3330
  data.name = namedString(marker, "Name", file) ?? key;
2960
3331
  data.required = !property.nullable;
2961
3332
  data.locked = namedBoolean(marker, "Locked", file) ?? false;
3333
+ const storage = namedString(marker, "Storage", file);
3334
+ if (storage !== void 0) {
3335
+ if (storage !== "static" && storage !== "save" && storage !== "session") {
3336
+ throw new SchemaParseError(
3337
+ `Storage must be "static", "save", or "session"; found "${storage}".`,
3338
+ file,
3339
+ marker.line,
3340
+ marker.column
3341
+ );
3342
+ }
3343
+ data.storage = storage;
3344
+ }
2962
3345
  const extendsId = namedString(marker, "ExtendsId", file);
2963
3346
  if (extendsId !== void 0) data.extendsAttributeId = extendsId;
2964
3347
  const defaultJson = parseJsonArg(marker, "DefaultJson", file);
@@ -2990,6 +3373,11 @@ function reconstructProperty(file, property, typeIdsByName, enumIdsByName, baseR
2990
3373
  if (localizable !== void 0) data.localizable = localizable;
2991
3374
  break;
2992
3375
  }
3376
+ case ATTRIBUTE_TYPE.Vector2:
3377
+ case ATTRIBUTE_TYPE.Vector2Int:
3378
+ case ATTRIBUTE_TYPE.Vector3:
3379
+ case ATTRIBUTE_TYPE.Vector3Int:
3380
+ break;
2993
3381
  case ATTRIBUTE_TYPE.Dictionary:
2994
3382
  case ATTRIBUTE_TYPE.List: {
2995
3383
  const entryChainJson = parseJsonArg(marker, "EntryChainJson", file);
@@ -3275,6 +3663,14 @@ function shapeToTrivialFields(shape, typeIdsByName, enumIdsByName, property, fil
3275
3663
  return { type: ATTRIBUTE_TYPE.String };
3276
3664
  case "float":
3277
3665
  return { type: ATTRIBUTE_TYPE.Float };
3666
+ case "NeoVector2":
3667
+ return { type: ATTRIBUTE_TYPE.Vector2 };
3668
+ case "NeoVector2Int":
3669
+ return { type: ATTRIBUTE_TYPE.Vector2Int };
3670
+ case "NeoVector3":
3671
+ return { type: ATTRIBUTE_TYPE.Vector3 };
3672
+ case "NeoVector3Int":
3673
+ return { type: ATTRIBUTE_TYPE.Vector3Int };
3278
3674
  case "IReadOnlyList":
3279
3675
  return { type: ATTRIBUTE_TYPE.List };
3280
3676
  case "IReadOnlyDictionary":
@@ -3686,6 +4082,12 @@ function emitTypeFile(context, typeRecord, warnings, reachableAttributeIds) {
3686
4082
  delete remaining.hiddenInAttributeSelector;
3687
4083
  if (data.hiddenInAttributeSelector) args.push("Hidden = true");
3688
4084
  }
4085
+ if (typeof data.allowedStorage === "string") {
4086
+ delete remaining.allowedStorage;
4087
+ args.push(`AllowedStorage = ${csString(data.allowedStorage)}`);
4088
+ } else if (data.allowedStorage === null) {
4089
+ delete remaining.allowedStorage;
4090
+ }
3689
4091
  const modifier = data.isAbstract === true ? "abstract" : "sealed";
3690
4092
  if (typeof data.isAbstract === "boolean") delete remaining.isAbstract;
3691
4093
  const schema = isObjectRecord2(data.schema) ? data.schema : {};
@@ -3822,6 +4224,12 @@ function emitProperty(context, schemaKey, attributeRecord, memberNames, warnings
3822
4224
  delete remaining.locked;
3823
4225
  if (data.locked) args.push("Locked = true");
3824
4226
  }
4227
+ if (typeof data.storage === "string" && data.storage !== "inherit") {
4228
+ delete remaining.storage;
4229
+ args.push(`Storage = ${csString(data.storage)}`);
4230
+ } else if (data.storage === "inherit") {
4231
+ delete remaining.storage;
4232
+ }
3825
4233
  const required = data.required === true;
3826
4234
  if (typeof data.required === "boolean") delete remaining.required;
3827
4235
  let csType = "object";
@@ -3865,6 +4273,18 @@ function emitProperty(context, schemaKey, attributeRecord, memberNames, warnings
3865
4273
  args.push(`Localizable = ${data.localizable ? "true" : "false"}`);
3866
4274
  }
3867
4275
  break;
4276
+ case ATTRIBUTE_TYPE.Vector2:
4277
+ csType = "NeoVector2";
4278
+ break;
4279
+ case ATTRIBUTE_TYPE.Vector2Int:
4280
+ csType = "NeoVector2Int";
4281
+ break;
4282
+ case ATTRIBUTE_TYPE.Vector3:
4283
+ csType = "NeoVector3";
4284
+ break;
4285
+ case ATTRIBUTE_TYPE.Vector3Int:
4286
+ csType = "NeoVector3Int";
4287
+ break;
3868
4288
  case ATTRIBUTE_TYPE.Dictionary:
3869
4289
  case ATTRIBUTE_TYPE.List: {
3870
4290
  const entryAttributeId = data.entryAttributeId;
@@ -4052,6 +4472,14 @@ function renderEntryChainType(context, chain, index) {
4052
4472
  return "string";
4053
4473
  case ATTRIBUTE_TYPE.Float:
4054
4474
  return "float";
4475
+ case ATTRIBUTE_TYPE.Vector2:
4476
+ return "NeoVector2";
4477
+ case ATTRIBUTE_TYPE.Vector2Int:
4478
+ return "NeoVector2Int";
4479
+ case ATTRIBUTE_TYPE.Vector3:
4480
+ return "NeoVector3";
4481
+ case ATTRIBUTE_TYPE.Vector3Int:
4482
+ return "NeoVector3Int";
4055
4483
  case ATTRIBUTE_TYPE.Dictionary:
4056
4484
  return `IReadOnlyDictionary<string, ${renderEntryChainType(context, chain, index + 1)}>`;
4057
4485
  case ATTRIBUTE_TYPE.List:
@@ -6129,6 +6557,43 @@ function resolveAssetMemberType(receiverType, memberName) {
6129
6557
  }
6130
6558
  return null;
6131
6559
  }
6560
+ function resolveVectorMemberType(receiverType, memberName) {
6561
+ const componentType = receiverType.type === 15 /* Vector2Int */ || receiverType.type === 17 /* Vector3Int */ ? 2 /* Int */ : receiverType.type === 14 /* Vector2 */ || receiverType.type === 16 /* Vector3 */ ? 4 /* Float */ : null;
6562
+ if (componentType === null) return null;
6563
+ if (memberName === "x" || memberName === "y") {
6564
+ return { type: componentType, required: true };
6565
+ }
6566
+ if (memberName === "z" && (receiverType.type === 16 /* Vector3 */ || receiverType.type === 17 /* Vector3Int */)) {
6567
+ return { type: componentType, required: true };
6568
+ }
6569
+ return null;
6570
+ }
6571
+ function vectorTypeForConstructorName(name) {
6572
+ switch (name) {
6573
+ case "Vector2":
6574
+ return 14 /* Vector2 */;
6575
+ case "Vector2Int":
6576
+ return 15 /* Vector2Int */;
6577
+ case "Vector3":
6578
+ return 16 /* Vector3 */;
6579
+ case "Vector3Int":
6580
+ return 17 /* Vector3Int */;
6581
+ default:
6582
+ return null;
6583
+ }
6584
+ }
6585
+ function vectorComponentCount(type) {
6586
+ if (type === 14 /* Vector2 */ || type === 15 /* Vector2Int */) {
6587
+ return 2;
6588
+ }
6589
+ return 3;
6590
+ }
6591
+ function vectorComponentType(type) {
6592
+ if (type === 15 /* Vector2Int */ || type === 17 /* Vector3Int */) {
6593
+ return 2 /* Int */;
6594
+ }
6595
+ return 4 /* Float */;
6596
+ }
6132
6597
  function describeType(t) {
6133
6598
  const opt = t.required ? "" : "?";
6134
6599
  switch (t.type) {
@@ -6148,6 +6613,14 @@ function describeType(t) {
6148
6613
  return `SpriteInfo${opt}`;
6149
6614
  case 12 /* Audio */:
6150
6615
  return `AudioClipInfo${opt}`;
6616
+ case 14 /* Vector2 */:
6617
+ return `Vector2${opt}`;
6618
+ case 15 /* Vector2Int */:
6619
+ return `Vector2Int${opt}`;
6620
+ case 16 /* Vector3 */:
6621
+ return `Vector3${opt}`;
6622
+ case 17 /* Vector3Int */:
6623
+ return `Vector3Int${opt}`;
6151
6624
  case 7 /* Custom */:
6152
6625
  return `Custom<${t.typeId}>${opt}`;
6153
6626
  case 8 /* Enum */:
@@ -6697,6 +7170,9 @@ var init_resolver = __esm({
6697
7170
  * (bool for Where/Find, inferred for Select).
6698
7171
  */
6699
7172
  expectedReturnStack = [];
7173
+ /** Lazy effective-storage machinery for writability classification. */
7174
+ storageResolver = null;
7175
+ rootStorageByAttributeId = null;
6700
7176
  compile(body) {
6701
7177
  const scope = new Scope(null);
6702
7178
  scope.define({
@@ -6851,8 +7327,9 @@ var init_resolver = __esm({
6851
7327
  }
6852
7328
  case "return": {
6853
7329
  const expectedReturn = this.currentExpectedReturn();
7330
+ const inActionBody = this.ctx.mode === "action" /* Action */ && this.lambdaDepth === 0;
6854
7331
  if (stmt.expr === null) {
6855
- if (this.ctx.mode === "action" /* Action */) {
7332
+ if (inActionBody) {
6856
7333
  return {
6857
7334
  type: "return" /* return */,
6858
7335
  pointer: null
@@ -6870,7 +7347,7 @@ var init_resolver = __esm({
6870
7347
  };
6871
7348
  }
6872
7349
  const resolved = this.resolveExpr(stmt.expr, scope, expectedReturn);
6873
- if (this.ctx.mode === "action" /* Action */) {
7350
+ if (inActionBody) {
6874
7351
  if (resolved.type.type !== 0 /* Null */) {
6875
7352
  throw new CompileError(
6876
7353
  `Action functions can only return null; got ${describeType(resolved.type)}`,
@@ -7444,70 +7921,153 @@ var init_resolver = __esm({
7444
7921
  pos
7445
7922
  );
7446
7923
  }
7447
- if (path === "root.Save" || path?.startsWith("root.Save.")) {
7448
- return "save" /* Save */;
7449
- }
7450
- if (path === "root.Session" || path?.startsWith("root.Session.")) {
7451
- return "session" /* Session */;
7452
- }
7453
- if (path === "root.Assets" || path?.startsWith("root.Assets.")) {
7454
- const lookupWritability = this.rootAssetsPathCrossesWritableLookup(
7455
- expr,
7456
- scope
7924
+ const rooted = this.resolveRootedTargetStorage(expr, scope);
7925
+ if (rooted !== null) {
7926
+ if (rooted.storage === "save" /* Save */) {
7927
+ return rooted.reRootedFromStatic ? "assetToSaveLookup" /* AssetToSaveLookup */ : "save" /* Save */;
7928
+ }
7929
+ if (rooted.storage === "session" /* Session */) {
7930
+ return rooted.reRootedFromStatic ? "assetToSessionLookup" /* AssetToSessionLookup */ : "session" /* Session */;
7931
+ }
7932
+ throw new CompileError(
7933
+ `Cannot assign to '${path}' because its effective storage is static.`,
7934
+ pos
7457
7935
  );
7458
- if (lookupWritability) return lookupWritability;
7936
+ }
7937
+ const declared = this.declaredStorageForMemberTarget(expr, scope, pos);
7938
+ if (declared === "static" /* Static */) {
7459
7939
  throw new CompileError(
7460
- `Cannot assign to '${path}' because asset values are read-only.`,
7940
+ `Cannot assign to '${path}' because its effective storage is static.`,
7461
7941
  pos
7462
7942
  );
7463
7943
  }
7944
+ if (declared === "session" /* Session */) return "session" /* Session */;
7464
7945
  return "save" /* Save */;
7465
7946
  }
7466
- validateNonGetterAssignmentTarget(expr, scope, pos) {
7467
- if (expr.kind !== "member") return;
7947
+ /**
7948
+ * Placement-based storage resolution for an assignment target rooted at a
7949
+ * `root.<Member>` path (specs/attribute-storage.md §5.1). Returns null for
7950
+ * targets not rooted at `root` — their placement is runtime-determined.
7951
+ *
7952
+ * Walks the member/index chain root-first, tracking the effective storage
7953
+ * context: each hop's declared storage (resolved through the extends
7954
+ * chain) replaces the context; Lookup hops on a Static context re-root the
7955
+ * context at the target collection's effective storage and mark the
7956
+ * `AssetTo*Lookup` classification (§2.4 / legacy
7957
+ * `rootAssetsPathCrossesWritableLookup` semantics).
7958
+ */
7959
+ resolveRootedTargetStorage(expr, scope) {
7960
+ if (expr.kind === "index") {
7961
+ const receiver = this.resolveRootedTargetStorage(expr.receiver, scope);
7962
+ if (receiver === null) return null;
7963
+ let storage2 = receiver.storage;
7964
+ let entryAttributeId = null;
7965
+ if (receiver.attributeId !== null) {
7966
+ const storageResolver2 = this.getStorageResolver();
7967
+ const directEntryId = storageResolver2.resolvedEntryAttributeId(
7968
+ receiver.attributeId
7969
+ );
7970
+ const lookupCollectionId = directEntryId === void 0 ? storageResolver2.resolvedCollectionAttributeId(
7971
+ receiver.attributeId
7972
+ ) : void 0;
7973
+ const entryId = directEntryId ?? (lookupCollectionId === void 0 ? void 0 : storageResolver2.resolvedEntryAttributeId(lookupCollectionId));
7974
+ if (entryId !== void 0) {
7975
+ entryAttributeId = entryId;
7976
+ const declared2 = storageResolver2.declaredStorage(entryId);
7977
+ if (declared2 !== "inherit" /* Inherit */) storage2 = declared2;
7978
+ }
7979
+ }
7980
+ return {
7981
+ storage: storage2,
7982
+ reRootedFromStatic: receiver.reRootedFromStatic,
7983
+ attributeId: entryAttributeId
7984
+ };
7985
+ }
7986
+ if (expr.kind !== "member") return null;
7987
+ if (expr.receiver.kind === "ident" && expr.receiver.name === "root") {
7988
+ const rootId = this.rootAttributeIdForMember(expr.name);
7989
+ if (rootId === null) return null;
7990
+ const storage2 = this.getRootStorageMap().get(rootId);
7991
+ if (storage2 === void 0) return null;
7992
+ return { storage: storage2, reRootedFromStatic: false, attributeId: rootId };
7993
+ }
7994
+ const parent = this.resolveRootedTargetStorage(expr.receiver, scope);
7995
+ if (parent === null) return null;
7468
7996
  const attr = this.resolveMemberAttribute(
7469
7997
  expr.receiver,
7470
7998
  expr.name,
7471
7999
  scope,
7472
- pos
8000
+ expr.pos
7473
8001
  );
7474
- if (attr?.type !== 10 /* NSGetter */) return;
7475
- throw new CompileError(
7476
- `Cannot assign to getter attribute '${attr.name}' because getter attributes are read-only.`,
7477
- pos
8002
+ if (attr === null) {
8003
+ return {
8004
+ storage: parent.storage,
8005
+ reRootedFromStatic: parent.reRootedFromStatic,
8006
+ attributeId: null
8007
+ };
8008
+ }
8009
+ const storageResolver = this.getStorageResolver();
8010
+ let storage = parent.storage;
8011
+ const declared = storageResolver.declaredStorage(attr.id);
8012
+ if (declared !== "inherit" /* Inherit */) storage = declared;
8013
+ let reRootedFromStatic = parent.reRootedFromStatic;
8014
+ if (storage === "static" /* Static */ && isAttributeLookupBase(attr)) {
8015
+ const collectionId = storageResolver.resolvedCollectionAttributeId(
8016
+ attr.id
8017
+ );
8018
+ if (collectionId !== void 0) {
8019
+ const targetStorage = this.lookupTargetStorage(collectionId);
8020
+ if (targetStorage === "save" /* Save */ || targetStorage === "session" /* Session */) {
8021
+ storage = targetStorage;
8022
+ reRootedFromStatic = true;
8023
+ }
8024
+ }
8025
+ }
8026
+ return { storage, reRootedFromStatic, attributeId: attr.id };
8027
+ }
8028
+ /**
8029
+ * Effective storage of a lookup's target collection across its placements.
8030
+ * Save wins over Session when a collection is placed in both (mirrors the
8031
+ * legacy save-root-first `attributeBelongsToRoot` check order).
8032
+ */
8033
+ lookupTargetStorage(collectionAttributeId) {
8034
+ const set = this.getStorageResolver().effectiveStorages(
8035
+ collectionAttributeId
7478
8036
  );
8037
+ if (set.has("save" /* Save */)) return "save" /* Save */;
8038
+ if (set.has("session" /* Session */)) return "session" /* Session */;
8039
+ if (set.has("static" /* Static */)) return "static" /* Static */;
8040
+ return null;
7479
8041
  }
7480
- rootAssetsPathCrossesWritableLookup(expr, scope) {
7481
- if (expr.kind === "index") {
7482
- return this.rootAssetsPathCrossesWritableLookup(expr.receiver, scope);
7483
- }
7484
- if (expr.kind !== "member") return null;
7485
- const receiverLookup = this.rootAssetsPathCrossesWritableLookup(
8042
+ /**
8043
+ * Declared storage (through the extends chain) of the final member
8044
+ * attribute of a non-rooted assignment target, or `Inherit` when the
8045
+ * target is not a member access / cannot be resolved.
8046
+ */
8047
+ declaredStorageForMemberTarget(expr, scope, pos) {
8048
+ if (expr.kind !== "member") return "inherit" /* Inherit */;
8049
+ const attr = this.resolveMemberAttribute(
7486
8050
  expr.receiver,
7487
- scope
8051
+ expr.name,
8052
+ scope,
8053
+ pos
7488
8054
  );
7489
- if (receiverLookup) return receiverLookup;
8055
+ if (attr === null) return "inherit" /* Inherit */;
8056
+ return this.getStorageResolver().declaredStorage(attr.id);
8057
+ }
8058
+ validateNonGetterAssignmentTarget(expr, scope, pos) {
8059
+ if (expr.kind !== "member") return;
7490
8060
  const attr = this.resolveMemberAttribute(
7491
8061
  expr.receiver,
7492
8062
  expr.name,
7493
8063
  scope,
7494
- expr.pos
8064
+ pos
8065
+ );
8066
+ if (attr?.type !== 10 /* NSGetter */) return;
8067
+ throw new CompileError(
8068
+ `Cannot assign to getter attribute '${attr.name}' because getter attributes are read-only.`,
8069
+ pos
7495
8070
  );
7496
- if (!attr) return null;
7497
- if (!isAttributeLookupBase(attr)) return null;
7498
- if (this.attributeBelongsToRoot(
7499
- this.ctx.project.rootSaveFileAttributeId,
7500
- attr.collectionAttributeId
7501
- )) {
7502
- return "assetToSaveLookup" /* AssetToSaveLookup */;
7503
- }
7504
- if (this.attributeBelongsToRoot(
7505
- this.ctx.project.rootSessionAttributeId,
7506
- attr.collectionAttributeId
7507
- )) {
7508
- return "assetToSessionLookup" /* AssetToSessionLookup */;
7509
- }
7510
- return null;
7511
8071
  }
7512
8072
  resolveMemberAttribute(receiverAst, name, scope, pos) {
7513
8073
  const receiver = this.resolveExpr(receiverAst, scope);
@@ -7529,45 +8089,28 @@ var init_resolver = __esm({
7529
8089
  if (!attributeId) return null;
7530
8090
  return this.ctx.attributes.find((a) => a.id === attributeId) ?? null;
7531
8091
  }
7532
- attributeBelongsToRoot(rootAttributeId, targetId) {
7533
- const visited = /* @__PURE__ */ new Set();
7534
- const stack = [rootAttributeId];
7535
- while (stack.length > 0) {
7536
- const attrId = stack.pop();
7537
- if (!attrId) continue;
7538
- if (attrId === targetId) return true;
7539
- if (visited.has(attrId)) continue;
7540
- visited.add(attrId);
7541
- const attr = this.ctx.attributes.find((a) => a.id === attrId);
7542
- if (!attr) continue;
7543
- if (isAttributeCustomBase(attr)) {
7544
- const customType = this.ctx.types.find(
7545
- (t) => t.id === attr.customTypeId
7546
- );
7547
- if (!customType) continue;
7548
- for (const entry of mergeSchemas(
7549
- resolveInheritanceChain(customType.id, this.ctx.types)
7550
- )) {
7551
- if (entry.attributeId !== null) stack.push(entry.attributeId);
7552
- }
7553
- continue;
7554
- }
7555
- if (isAttributeListBase(attr) || isAttributeDictionaryBase(attr)) {
7556
- stack.push(attr.entryAttributeId);
7557
- continue;
7558
- }
7559
- if (isAttributeLookupBase(attr)) {
7560
- stack.push(attr.collectionAttributeId);
7561
- }
7562
- }
7563
- return false;
7564
- }
7565
8092
  rootAttributeIdForMember(name) {
7566
8093
  const root = projectRootAttributesInDisplayOrder(this.ctx.project).find(
7567
8094
  (entry) => entry.root.key === name
7568
8095
  );
7569
8096
  return root?.attributeId ?? null;
7570
8097
  }
8098
+ getRootStorageMap() {
8099
+ if (this.rootStorageByAttributeId === null) {
8100
+ this.rootStorageByAttributeId = buildRootStorageMap(this.ctx.project);
8101
+ }
8102
+ return this.rootStorageByAttributeId;
8103
+ }
8104
+ getStorageResolver() {
8105
+ if (this.storageResolver === null) {
8106
+ this.storageResolver = new EffectiveStorageResolver({
8107
+ attributes: this.ctx.attributes,
8108
+ types: this.ctx.types,
8109
+ rootStorage: this.getRootStorageMap()
8110
+ });
8111
+ }
8112
+ return this.storageResolver;
8113
+ }
7571
8114
  invalidateNarrowingForAssignment(target, scope) {
7572
8115
  const path = canonicalPath(target);
7573
8116
  if (!path) return;
@@ -7749,6 +8292,20 @@ var init_resolver = __esm({
7749
8292
  type: optional ? withRequired(memberType, false) : memberType
7750
8293
  };
7751
8294
  }
8295
+ const vectorMemberType = resolveVectorMemberType(receiver.type, name);
8296
+ if (vectorMemberType !== null) {
8297
+ return {
8298
+ pointer: {
8299
+ type: "keyOf" /* keyOf */,
8300
+ keyOf: {
8301
+ pointer: receiver.pointer,
8302
+ key: stringPointer(name)
8303
+ },
8304
+ ...optional ? { optional: true } : {}
8305
+ },
8306
+ type: optional ? withRequired(vectorMemberType, false) : vectorMemberType
8307
+ };
8308
+ }
7752
8309
  if (receiver.type.type === 7 /* Custom */) {
7753
8310
  const typeId = receiver.type.typeId;
7754
8311
  if (typeId === ROOT_TYPE_ID) {
@@ -8202,6 +8759,10 @@ var init_resolver = __esm({
8202
8759
  );
8203
8760
  }
8204
8761
  resolveGlobalCall(name, args, scope, pos) {
8762
+ const vectorType = vectorTypeForConstructorName(name);
8763
+ if (vectorType !== null) {
8764
+ return this.resolveVectorConstructorCall(vectorType, args, scope, pos);
8765
+ }
8205
8766
  const functionType = name === "VisitCount" ? "visitCount" /* visitCount */ : name === "HasVisited" ? "hasVisited" /* hasVisited */ : null;
8206
8767
  if (!functionType) {
8207
8768
  throw new CompileError(`Unknown function '${name}'`, pos);
@@ -8230,20 +8791,53 @@ var init_resolver = __esm({
8230
8791
  type: functionType === "visitCount" /* visitCount */ ? { type: 2 /* Int */, required: true } : { type: 1 /* Bool */, required: true }
8231
8792
  };
8232
8793
  }
8233
- resolveCollectionMutationStatement(expr, scope, pos) {
8234
- const callee = expr.callee;
8235
- if (callee.kind !== "member") {
8794
+ resolveVectorConstructorCall(vectorType, args, scope, pos) {
8795
+ const expectedCount = vectorComponentCount(vectorType);
8796
+ if (args.length !== expectedCount) {
8236
8797
  throw new CompileError(
8237
- "Collection mutation calls must use member-call form.",
8798
+ `${AttributeType[vectorType]} takes ${expectedCount} numeric arguments, got ${args.length}`,
8238
8799
  pos
8239
8800
  );
8240
8801
  }
8241
- const receiver = this.resolveExpr(callee.receiver, scope);
8242
- if (receiver.type.type !== 6 /* List */ && receiver.type.type !== 5 /* Dictionary */ && receiver.type.type !== 9 /* Lookup */) {
8243
- throw new CompileError(
8244
- `Cannot call ${callee.name} on ${describeType(receiver.type)}; expected list, dictionary, or lookup set.`,
8245
- pos
8246
- );
8802
+ const scalarType = vectorComponentType(vectorType);
8803
+ const expectedType = { type: scalarType, required: true };
8804
+ const resolved = args.map((arg) => this.resolveExpr(arg, scope));
8805
+ for (const arg of resolved) {
8806
+ if (!isAssignable(arg.type, expectedType, this.ctx.types)) {
8807
+ throw new CompileError(
8808
+ `${AttributeType[vectorType]} component must be ${describeType(expectedType)}; got ${describeType(arg.type)}`,
8809
+ pos
8810
+ );
8811
+ }
8812
+ }
8813
+ return {
8814
+ pointer: {
8815
+ type: "function" /* function */,
8816
+ function: {
8817
+ type: "vectorConstructor" /* vectorConstructor */,
8818
+ info: {
8819
+ vectorType,
8820
+ componentPointers: resolved.map((arg) => arg.pointer)
8821
+ }
8822
+ }
8823
+ },
8824
+ type: { type: vectorType, required: true }
8825
+ };
8826
+ }
8827
+ resolveCollectionMutationStatement(expr, scope, pos) {
8828
+ const callee = expr.callee;
8829
+ if (callee.kind !== "member") {
8830
+ throw new CompileError(
8831
+ "Collection mutation calls must use member-call form.",
8832
+ pos
8833
+ );
8834
+ }
8835
+ const receiver = this.resolveExpr(callee.receiver, scope);
8836
+ if (receiver.type.type !== 6 /* List */ && receiver.type.type !== 5 /* Dictionary */ && receiver.type.type !== 9 /* Lookup */) {
8837
+ throw new CompileError(
8838
+ `Cannot call ${callee.name} on ${describeType(receiver.type)}; expected list, dictionary, or lookup set.`,
8839
+ pos
8840
+ );
8247
8841
  }
8248
8842
  const localMutation = localCollectionMutationTargetEntry(
8249
8843
  callee.receiver,
@@ -8618,6 +9212,18 @@ var init_resolver = __esm({
8618
9212
  required: t.required
8619
9213
  };
8620
9214
  }
9215
+ if (t.name === "Vector2") {
9216
+ return { type: 14 /* Vector2 */, required: t.required };
9217
+ }
9218
+ if (t.name === "Vector2Int") {
9219
+ return { type: 15 /* Vector2Int */, required: t.required };
9220
+ }
9221
+ if (t.name === "Vector3") {
9222
+ return { type: 16 /* Vector3 */, required: t.required };
9223
+ }
9224
+ if (t.name === "Vector3Int") {
9225
+ return { type: 17 /* Vector3Int */, required: t.required };
9226
+ }
8621
9227
  const customType = this.ctx.types.find((ct) => ct.name === t.name);
8622
9228
  if (customType) {
8623
9229
  return {
@@ -8682,6 +9288,14 @@ var init_resolver = __esm({
8682
9288
  return { type: 11 /* Sprite */, required };
8683
9289
  case 12 /* Audio */:
8684
9290
  return { type: 12 /* Audio */, required };
9291
+ case 14 /* Vector2 */:
9292
+ return { type: 14 /* Vector2 */, required };
9293
+ case 15 /* Vector2Int */:
9294
+ return { type: 15 /* Vector2Int */, required };
9295
+ case 16 /* Vector3 */:
9296
+ return { type: 16 /* Vector3 */, required };
9297
+ case 17 /* Vector3Int */:
9298
+ return { type: 17 /* Vector3Int */, required };
8685
9299
  case 7 /* Custom */:
8686
9300
  if (!isAttributeCustomBase(attr)) {
8687
9301
  throw new CompileError(
@@ -9022,6 +9636,16 @@ ${status.parseErrors.map((error) => ` ${error.message}`).join("\n")}`
9022
9636
  action: compileMigrationAction(status, change.nextData)
9023
9637
  };
9024
9638
  }
9639
+ if (change.recordKind === "attribute" && change.nextData.type === 10 && typeof change.nextData.code === "string" && change.nextData.code.trim().length > 0) {
9640
+ change.nextData = {
9641
+ ...change.nextData,
9642
+ getter: await compileAttributeGetter(
9643
+ workspace,
9644
+ status,
9645
+ change.nextData
9646
+ )
9647
+ };
9648
+ }
9025
9649
  if (change.kind === "create") {
9026
9650
  change.nextData = {
9027
9651
  ...change.nextData,
@@ -9231,6 +9855,35 @@ function rewriteFilesFromState(workspace) {
9231
9855
  workspace.state.records[key] = nextState;
9232
9856
  }
9233
9857
  }
9858
+ async function compileAttributeGetter(workspace, status, attributeData) {
9859
+ const types = [];
9860
+ const attributes = [];
9861
+ const enums = [];
9862
+ for (const record of status.reconstructed.values()) {
9863
+ if (record.recordKind === "type") types.push(record.fullData);
9864
+ else if (record.recordKind === "attribute")
9865
+ attributes.push(record.fullData);
9866
+ else if (record.recordKind === "enum") enums.push(record.fullData);
9867
+ }
9868
+ const attributeId = attributeData.id;
9869
+ const thisType = types.find((type) => {
9870
+ const schema = type.schema;
9871
+ return typeof schema === "object" && schema !== null && Object.values(schema).includes(attributeId);
9872
+ }) ?? null;
9873
+ const { raw } = await fetchProjectDocument(workspace);
9874
+ if (typeof raw.project !== "object" || raw.project === null) {
9875
+ throw new Error("Cannot compile getter: project record is unavailable.");
9876
+ }
9877
+ return compileNSGetter(String(attributeData.code), {
9878
+ project: raw.project,
9879
+ attributes,
9880
+ types,
9881
+ enums,
9882
+ thisType,
9883
+ returnTypeInfo: attributeData.returnTypeInfo,
9884
+ dialogueContext: null
9885
+ });
9886
+ }
9234
9887
  function compileMigrationAction(status, migrationData) {
9235
9888
  const types = [];
9236
9889
  const attributes = [];
@@ -9257,6 +9910,7 @@ var init_push = __esm({
9257
9910
  "src/commands/push.ts"() {
9258
9911
  "use strict";
9259
9912
  init_neoscript2();
9913
+ init_document();
9260
9914
  init_convex();
9261
9915
  init_ui();
9262
9916
  init_project_version_intents();
@@ -9638,6 +10292,10 @@ function numberArg(value, name) {
9638
10292
  if (typeof value === "number" && Number.isInteger(value)) return value;
9639
10293
  throw new NSGetterRuntimeError(`${name} must be an integer`);
9640
10294
  }
10295
+ function finiteNumberArg(value, name) {
10296
+ if (typeof value === "number" && Number.isFinite(value)) return value;
10297
+ throw new NSGetterRuntimeError(`${name} must be a finite number`);
10298
+ }
9641
10299
  function evalPointer(pointer, scope, ctx) {
9642
10300
  switch (pointer.type) {
9643
10301
  case "value" /* value */:
@@ -9900,6 +10558,14 @@ function runtimeTypeCheck(value, checkType, ctx) {
9900
10558
  return isRuntimeSpriteValue(value);
9901
10559
  case 12 /* Audio */:
9902
10560
  return isRuntimeFileValue(value) && !isRuntimeSpriteValue(value);
10561
+ case 14 /* Vector2 */:
10562
+ return isVector2Value(value);
10563
+ case 15 /* Vector2Int */:
10564
+ return isVector2IntValue(value);
10565
+ case 16 /* Vector3 */:
10566
+ return isVector3Value(value);
10567
+ case 17 /* Vector3Int */:
10568
+ return isVector3IntValue(value);
9903
10569
  case 6 /* List */:
9904
10570
  case 9 /* Lookup */:
9905
10571
  return Array.isArray(value);
@@ -9921,6 +10587,24 @@ function runtimeTypeCheck(value, checkType, ctx) {
9921
10587
  return false;
9922
10588
  }
9923
10589
  }
10590
+ function evalVectorConstructor(vectorType, componentPointers, scope, ctx) {
10591
+ const values = componentPointers.map(
10592
+ (pointer) => finiteNumberArg(
10593
+ evalPointer(pointer, scope, ctx),
10594
+ `${AttributeType[vectorType]} component`
10595
+ )
10596
+ );
10597
+ const integer = vectorType === 15 /* Vector2Int */ || vectorType === 17 /* Vector3Int */;
10598
+ if (integer && values.some((value) => !Number.isInteger(value))) {
10599
+ throw new NSGetterRuntimeError(
10600
+ `${AttributeType[vectorType]} components must be integers`
10601
+ );
10602
+ }
10603
+ if (vectorType === 14 /* Vector2 */ || vectorType === 15 /* Vector2Int */) {
10604
+ return { x: values[0], y: values[1] };
10605
+ }
10606
+ return { x: values[0], y: values[1], z: values[2] };
10607
+ }
9924
10608
  function isRuntimeFileValue(value) {
9925
10609
  if (typeof value !== "object") return false;
9926
10610
  if (value === null) return false;
@@ -10304,6 +10988,14 @@ function evalFunction(fn, scope, ctx) {
10304
10988
  const pointer = evalPointer(fn.info.pointer, scope, ctx);
10305
10989
  return dialogueMemoryVisitCount(pointer, ctx) > 0;
10306
10990
  }
10991
+ case "vectorConstructor" /* vectorConstructor */: {
10992
+ return evalVectorConstructor(
10993
+ fn.info.vectorType,
10994
+ fn.info.componentPointers,
10995
+ scope,
10996
+ ctx
10997
+ );
10998
+ }
10307
10999
  case "count" /* count */: {
10308
11000
  const c = evalPointer(fn.info.collectionPointer, scope, ctx);
10309
11001
  return collectionLength(c);
@@ -10645,13 +11337,34 @@ function parseReturns(returns, document) {
10645
11337
  if (returns === null) {
10646
11338
  return RETURN_SHORTHANDS.unknown;
10647
11339
  }
10648
- const shorthand = RETURN_SHORTHANDS[returns];
10649
- if (shorthand !== void 0) return shorthand;
10650
- if (returns.trim().startsWith("{")) {
10651
- return JSON.parse(returns);
11340
+ const trimmed = returns.trim();
11341
+ if (trimmed.startsWith("{")) {
11342
+ return JSON.parse(trimmed);
11343
+ }
11344
+ return parseReturnExpression(trimmed, document);
11345
+ }
11346
+ function parseReturnExpression(expression, document) {
11347
+ if (expression.endsWith("[]")) {
11348
+ return {
11349
+ // AttributeType.List
11350
+ type: 6,
11351
+ required: true,
11352
+ entryTypeInfo: parseReturnExpression(
11353
+ expression.slice(0, -2).trim(),
11354
+ document
11355
+ )
11356
+ };
11357
+ }
11358
+ if (expression.endsWith("?")) {
11359
+ return {
11360
+ ...parseReturnExpression(expression.slice(0, -1).trim(), document),
11361
+ required: false
11362
+ };
10652
11363
  }
11364
+ const shorthand = RETURN_SHORTHANDS[expression];
11365
+ if (shorthand !== void 0) return shorthand;
10653
11366
  const type = document.types.find(
10654
- (entry) => entry.name === returns || entry.id === returns
11367
+ (entry) => entry.name === expression || entry.id === expression
10655
11368
  );
10656
11369
  if (type !== void 0) {
10657
11370
  return {
@@ -10661,7 +11374,7 @@ function parseReturns(returns, document) {
10661
11374
  };
10662
11375
  }
10663
11376
  const enumRecord = document.enums.find(
10664
- (entry) => entry.name === returns || entry.id === returns
11377
+ (entry) => entry.name === expression || entry.id === expression
10665
11378
  );
10666
11379
  if (enumRecord !== void 0) {
10667
11380
  return {
@@ -10671,7 +11384,7 @@ function parseReturns(returns, document) {
10671
11384
  };
10672
11385
  }
10673
11386
  throw new Error(
10674
- `--returns "${returns}" is not a shorthand (bool|int|string|float|unknown), a known type/enum name, or TNSTypeInfo JSON.`
11387
+ `--returns "${expression}" is not a shorthand (bool|int|string|float|unknown, with [] / ? suffixes), a known type/enum name, or TNSTypeInfo JSON.`
10675
11388
  );
10676
11389
  }
10677
11390
  function resolveThisType(thisRef, document) {
@@ -10773,8 +11486,9 @@ async function runScript(workspace, command, options) {
10773
11486
  line: error.line,
10774
11487
  column: error.column
10775
11488
  };
11489
+ const hint = options.returns === null && error.message.includes("not assignable to declared return type") ? '\n(eval defaults to an unknown return type \u2014 pass --returns, e.g. --returns "string[]")' : "";
10776
11490
  console.log(
10777
- options.json ? JSON.stringify(report2, null, 2) : `Compile error at ${error.line}:${error.column} \u2014 ${error.message}`
11491
+ options.json ? JSON.stringify(report2, null, 2) : `Compile error at ${error.line}:${error.column} \u2014 ${error.message}${hint}`
10778
11492
  );
10779
11493
  process.exitCode = 1;
10780
11494
  return;
@@ -10787,6 +11501,10 @@ async function runScript(workspace, command, options) {
10787
11501
  );
10788
11502
  return;
10789
11503
  }
11504
+ if (command === "compile") {
11505
+ console.log(JSON.stringify(compiled, null, 2));
11506
+ return;
11507
+ }
10790
11508
  let evalValues = document.values;
10791
11509
  if (options.save !== null) {
10792
11510
  if (command === "apply") {
@@ -10814,7 +11532,7 @@ async function runScript(workspace, command, options) {
10814
11532
  )
10815
11533
  };
10816
11534
  const overlaidDocument = { ...document, values: evalValues };
10817
- const evaluatorContext = {
11535
+ const evaluatorContext2 = {
10818
11536
  vm,
10819
11537
  thisValue: resolveThisValue(options.thisRef, overlaidDocument),
10820
11538
  rootValue: buildRootValue(overlaidDocument),
@@ -10823,14 +11541,14 @@ async function runScript(workspace, command, options) {
10823
11541
  if (command === "eval") {
10824
11542
  const value = evaluateNSGetter(
10825
11543
  compiled,
10826
- evaluatorContext
11544
+ evaluatorContext2
10827
11545
  );
10828
11546
  console.log(
10829
11547
  options.json ? JSON.stringify({ ok: true, value }, null, 2) : JSON.stringify(value, null, 2)
10830
11548
  );
10831
11549
  return;
10832
11550
  }
10833
- const result = evaluateNSAction(compiled, evaluatorContext);
11551
+ const result = evaluateNSAction(compiled, evaluatorContext2);
10834
11552
  const report = { ok: true, writes: result.writes };
10835
11553
  console.log(
10836
11554
  options.json ? JSON.stringify(report, null, 2) : `${result.writes.length} write intent(s):
@@ -11811,15 +12529,18 @@ async function runBranch(workspace, subcommand, positional, fromRef) {
11811
12529
  console.log(JSON.stringify(versions, null, 2));
11812
12530
  return;
11813
12531
  }
12532
+ const showArchived = process.argv.includes("--all");
11814
12533
  for (const version of versions) {
11815
- if (version.archivedAt !== null && version.archivedAt !== void 0) {
11816
- continue;
11817
- }
12534
+ const archived = version.archivedAt !== null && version.archivedAt !== void 0;
12535
+ if (archived && !showArchived) continue;
11818
12536
  const current = version.id === workspace.config.versionId;
11819
12537
  const marker = current ? color.green("*") : " ";
11820
12538
  const kind = version.kind === "branch" ? `${sym.branch} branch ` : `${sym.tag} version`;
11821
12539
  const label = current ? color.bold(displayLabel(version)) : displayLabel(version);
11822
- console.log(`${marker} ${kind} ${label} ${color.dim(version.id)}`);
12540
+ const suffix = archived ? ` ${color.dim("(archived)")}` : "";
12541
+ console.log(
12542
+ `${marker} ${kind} ${label} ${color.dim(version.id)}${suffix}`
12543
+ );
11823
12544
  }
11824
12545
  return;
11825
12546
  }
@@ -12116,6 +12837,11 @@ async function runMerge(workspace, sourceRef, dryRun, runMigrations = false) {
12116
12837
  success(
12117
12838
  `Merged ${color.bold(String(merged))} record(s) from ${displayLabel(source)}.`
12118
12839
  );
12840
+ if (result.sourceArchived === true) {
12841
+ console.log(
12842
+ `Archived ${displayLabel(source)} \u2014 restore it with "neo branch restore ${typeof source.name === "string" ? source.name : source.id}".`
12843
+ );
12844
+ }
12119
12845
  if (merged > 0) {
12120
12846
  console.log("Pulling merged state...");
12121
12847
  await runPull(workspace, { force: false });
@@ -12274,141 +13000,1035 @@ var init_branch = __esm({
12274
13000
  }
12275
13001
  });
12276
13002
 
12277
- // src/commands/content.ts
12278
- var content_exports = {};
12279
- __export(content_exports, {
12280
- makeContentContext: () => makeContentContext,
12281
- runDialogue: () => runDialogue,
12282
- runFiles: () => runFiles,
12283
- runLoc: () => runLoc,
12284
- runRecords: () => runRecords,
12285
- runValues: () => runValues
13003
+ // src/commands/dialogue-spec.ts
13004
+ var dialogue_spec_exports = {};
13005
+ __export(dialogue_spec_exports, {
13006
+ runDialogueApply: () => runDialogueApply,
13007
+ runDialogueExport: () => runDialogueExport
12286
13008
  });
13009
+ import { randomUUID as randomUUID4 } from "node:crypto";
12287
13010
  import { readFileSync as readFileSync9 } from "node:fs";
12288
- function versionPath(workspace, suffix) {
12289
- return `/api/projects/${workspace.config.projectId}/versions/${workspace.config.versionId}/${suffix}`;
12290
- }
12291
- function readBatch(file) {
12292
- let raw = null;
12293
- if (file !== null) {
12294
- raw = readFileSync9(file, "utf8");
12295
- } else if (!process.stdin.isTTY) {
12296
- raw = readFileSync9(0, "utf8");
12297
- if (raw.trim().length === 0) raw = null;
12298
- }
12299
- if (raw === null) return null;
12300
- const parsed = JSON.parse(raw);
12301
- if (!Array.isArray(parsed)) {
12302
- throw new Error("Batch input must be a JSON array.");
13011
+ async function runDialogueExport(workspace, ref) {
13012
+ const { raw } = await fetchProjectDocument(workspace);
13013
+ const dialogues = Array.isArray(raw.dialogues) ? raw.dialogues.filter(isObjectRecord2) : [];
13014
+ const dialogue = dialogues.find(
13015
+ (entry) => entry.id === ref || entry.name === ref
13016
+ );
13017
+ if (dialogue === void 0) {
13018
+ throw new Error(`No dialogue matches "${ref}".`);
13019
+ }
13020
+ const locById = /* @__PURE__ */ new Map();
13021
+ const texts = Array.isArray(raw.localizedTexts) ? raw.localizedTexts.filter(isObjectRecord2) : [];
13022
+ for (const text of texts) {
13023
+ const locales = isObjectRecord2(text.localeValues) ? text.localeValues : {};
13024
+ const main2 = isObjectRecord2(locales["en-US"]) ? locales["en-US"] : {};
13025
+ if (typeof main2.value === "string") {
13026
+ locById.set(String(text.id), main2.value);
13027
+ }
13028
+ }
13029
+ const nodes = isObjectRecord2(dialogue.nodes) ? dialogue.nodes : {};
13030
+ const keyById = /* @__PURE__ */ new Map();
13031
+ const used = /* @__PURE__ */ new Set();
13032
+ for (const [nodeId, node] of Object.entries(nodes)) {
13033
+ let key = typeof node.name === "string" && node.name.length > 0 ? node.name : nodeId.slice(0, 8);
13034
+ while (used.has(key)) key = `${key}_`;
13035
+ used.add(key);
13036
+ keyById.set(nodeId, key);
13037
+ }
13038
+ const refKey = (nodeId) => typeof nodeId === "string" ? keyById.get(nodeId) ?? null : null;
13039
+ const specNodes = {};
13040
+ for (const [nodeId, node] of Object.entries(nodes)) {
13041
+ const spec = {};
13042
+ if (typeof node.text === "string") {
13043
+ spec.say = locById.get(node.text) ?? "";
13044
+ }
13045
+ const optionSettings = isObjectRecord2(node.optionSettings) ? node.optionSettings : null;
13046
+ const options = Array.isArray(optionSettings?.options) ? optionSettings.options.filter(isObjectRecord2) : [];
13047
+ if (options.length > 0) {
13048
+ spec.options = options.map((option) => ({
13049
+ label: option.name ?? "",
13050
+ text: typeof option.text === "string" ? locById.get(option.text) ?? "" : "",
13051
+ to: refKey(option.toNodeId)
13052
+ }));
13053
+ }
13054
+ const actions = Array.isArray(node.actions) ? node.actions.filter(isObjectRecord2) : [];
13055
+ if (actions.length > 0 || node.type === 2) {
13056
+ spec.do = actions.map((action) => {
13057
+ const logic = isObjectRecord2(action.logic) ? action.logic : {};
13058
+ const bind = typeof node.primaryLinkedValueId === "string" ? node.primaryLinkedValueId : void 0;
13059
+ if (typeof logic.code === "string") {
13060
+ return { code: logic.code, bind };
13061
+ }
13062
+ return { ir: logic.action ?? null, bind };
13063
+ });
13064
+ }
13065
+ if (Array.isArray(node.outcomes)) spec.outcomes = node.outcomes;
13066
+ if (typeof node.toNodeId === "string") spec.to = refKey(node.toNodeId);
13067
+ specNodes[keyById.get(nodeId) ?? nodeId] = spec;
12303
13068
  }
12304
- return parsed;
13069
+ const trigger = isObjectRecord2(dialogue.triggerNode) ? dialogue.triggerNode : null;
13070
+ const groupSettings = trigger !== null && isObjectRecord2(trigger.dialogueGroupSettings) ? trigger.dialogueGroupSettings : null;
13071
+ const priority = groupSettings !== null && isObjectRecord2(groupSettings.priority) ? groupSettings.priority : null;
13072
+ const occurrence = trigger !== null && isObjectRecord2(trigger.occurrenceLimitSettings) ? trigger.occurrenceLimitSettings : null;
13073
+ console.log(
13074
+ JSON.stringify(
13075
+ {
13076
+ id: dialogue.id,
13077
+ name: dialogue.name,
13078
+ description: dialogue.description ?? null,
13079
+ linkedValueId: dialogue.primaryLinkedValueId ?? null,
13080
+ group: groupSettings === null ? null : {
13081
+ id: groupSettings.dialogueGroupId,
13082
+ priority: priority?.priorityTypeId ?? null,
13083
+ relativeOrder: priority?.relativeOrder ?? 0,
13084
+ occurrenceLimit: occurrence?.count ?? null
13085
+ },
13086
+ when: (() => {
13087
+ const conditions = Array.isArray(trigger?.conditions) ? trigger.conditions.filter(isObjectRecord2) : [];
13088
+ const first = conditions[0];
13089
+ return first !== void 0 && typeof first.code === "string" ? first.code : null;
13090
+ })(),
13091
+ start: refKey(trigger?.toNodeId),
13092
+ nodes: specNodes
13093
+ },
13094
+ null,
13095
+ 2
13096
+ )
13097
+ );
12305
13098
  }
12306
- async function runRecords(context, subcommand, positional, filterKind) {
12307
- const { raw, records, heads } = await fetchProjectDocument(context.workspace);
12308
- if (subcommand === "get") {
12309
- const [recordKind, recordId] = positional;
12310
- if (recordKind === void 0 || recordId === void 0) {
12311
- throw new Error("Usage: neo records get <recordKind> <recordId>");
13099
+ async function runDialogueApply(workspace, file, dryRun) {
13100
+ const spec = JSON.parse(readFileSync9(file, "utf8"));
13101
+ if (!isObjectRecord2(spec)) throw new Error("Spec must be a JSON object.");
13102
+ if (typeof spec.name !== "string") throw new Error('Spec needs a "name".');
13103
+ if (!isObjectRecord2(spec.nodes)) throw new Error('Spec needs "nodes".');
13104
+ if (typeof spec.start !== "string") throw new Error('Spec needs "start".');
13105
+ const linkedValueId = typeof spec.linkedValueId === "string" ? spec.linkedValueId : null;
13106
+ if (linkedValueId === null) {
13107
+ throw new Error('Spec needs "linkedValueId" (the bound value id).');
13108
+ }
13109
+ const client = NeoApiClient.forBaseUrl(workspace.config.apiBaseUrl);
13110
+ const base = `/api/projects/${workspace.config.projectId}/versions/${workspace.config.versionId}`;
13111
+ const ids = {};
13112
+ for (const key of Object.keys(spec.nodes)) ids[key] = randomUUID4();
13113
+ if (ids[spec.start] === void 0) {
13114
+ throw new Error(`"start" references unknown node "${spec.start}".`);
13115
+ }
13116
+ const pendingTexts = [];
13117
+ const nodes = {};
13118
+ let index = 0;
13119
+ for (const [key, rawNode] of Object.entries(spec.nodes)) {
13120
+ if (!isObjectRecord2(rawNode)) {
13121
+ throw new Error(`Node "${key}" must be an object.`);
13122
+ }
13123
+ const nodeId = ids[key];
13124
+ const layout = {
13125
+ x: index % 4 * 380,
13126
+ y: 220 + Math.floor(index / 4) * 240,
13127
+ width: 320,
13128
+ height: 160,
13129
+ isOpen: true
13130
+ };
13131
+ index += 1;
13132
+ const resolveTo = (target) => {
13133
+ if (target === null || target === void 0) return void 0;
13134
+ const id = ids[String(target)];
13135
+ if (id === void 0) {
13136
+ throw new Error(
13137
+ `Node "${key}" points at unknown node "${String(target)}".`
13138
+ );
13139
+ }
13140
+ return id;
13141
+ };
13142
+ if (Array.isArray(rawNode.do)) {
13143
+ const groups = [];
13144
+ for (const step of rawNode.do) {
13145
+ if (!isObjectRecord2(step)) {
13146
+ throw new Error(`Node "${key}" has a non-object "do" step.`);
13147
+ }
13148
+ const bind = typeof step.bind === "string" ? step.bind : linkedValueId;
13149
+ const last = groups[groups.length - 1];
13150
+ if (last !== void 0 && last.bind === bind) {
13151
+ last.steps.push(step);
13152
+ } else {
13153
+ groups.push({ bind, steps: [step] });
13154
+ }
13155
+ }
13156
+ let chainNodeId = nodeId;
13157
+ for (const [index2, group2] of groups.entries()) {
13158
+ const actions = [];
13159
+ for (const step of group2.steps) {
13160
+ let ir = step.ir;
13161
+ if (typeof step.code === "string") {
13162
+ const compiled = await client.post(
13163
+ `${base}/dialogue-logic/compile`,
13164
+ {
13165
+ code: step.code,
13166
+ target: 1,
13167
+ dialogue: {},
13168
+ node: { primaryLinkedValueId: group2.bind }
13169
+ }
13170
+ );
13171
+ ir = compiled.action;
13172
+ }
13173
+ if (!isObjectRecord2(ir)) {
13174
+ throw new Error(`Node "${key}" "do" step needs "code" or "ir".`);
13175
+ }
13176
+ actions.push({
13177
+ id: randomUUID4(),
13178
+ type: 0,
13179
+ logic: typeof step.code === "string" ? { type: 1, code: step.code, action: ir } : { type: 0, action: ir }
13180
+ });
13181
+ }
13182
+ const isLast = index2 === groups.length - 1;
13183
+ const nextChainId = isLast ? void 0 : randomUUID4();
13184
+ nodes[chainNodeId] = {
13185
+ id: chainNodeId,
13186
+ type: 2,
13187
+ name: index2 === 0 ? key : `${key} (${index2 + 1})`,
13188
+ layout,
13189
+ linkedValues: [.../* @__PURE__ */ new Set([linkedValueId, group2.bind])].map(
13190
+ (valueId) => ({ source: 1, valueId })
13191
+ ),
13192
+ primaryLinkedValueId: group2.bind,
13193
+ actions,
13194
+ ...isLast ? resolveTo(rawNode.to) !== void 0 ? { toNodeId: resolveTo(rawNode.to) } : {} : { toNodeId: nextChainId }
13195
+ };
13196
+ if (nextChainId !== void 0) chainNodeId = nextChainId;
13197
+ }
13198
+ continue;
12312
13199
  }
12313
- const fromMap = records.get(`${recordKind}:${recordId}`);
12314
- if (fromMap !== void 0) {
12315
- console.log(JSON.stringify(fromMap, null, 2));
12316
- return;
13200
+ if (typeof rawNode.say !== "string") {
13201
+ throw new Error(`Node "${key}" needs "say" (text) or "do" (actions).`);
12317
13202
  }
12318
- for (const value of Object.values(raw)) {
12319
- if (!Array.isArray(value)) continue;
12320
- for (const entry of value) {
12321
- if (isObjectRecord2(entry) && entry.id === recordId) {
12322
- console.log(JSON.stringify(entry, null, 2));
12323
- return;
13203
+ const textId = randomUUID4();
13204
+ pendingTexts.push({
13205
+ textId,
13206
+ value: rawNode.say,
13207
+ nodeId,
13208
+ choiceId: null
13209
+ });
13210
+ const node = {
13211
+ id: nodeId,
13212
+ type: 1,
13213
+ name: key,
13214
+ layout,
13215
+ linkedValues: [{ source: 1, valueId: linkedValueId }],
13216
+ primaryLinkedValueId: linkedValueId,
13217
+ text: textId,
13218
+ variables: null
13219
+ };
13220
+ if (Array.isArray(rawNode.options) && rawNode.options.length > 0) {
13221
+ const options = [];
13222
+ for (const rawOption of rawNode.options) {
13223
+ if (!isObjectRecord2(rawOption)) {
13224
+ throw new Error(`Node "${key}" has a non-object option.`);
13225
+ }
13226
+ const option = rawOption;
13227
+ const optionId = randomUUID4();
13228
+ const optionTextId = randomUUID4();
13229
+ let optionSettings = null;
13230
+ if (typeof option.if === "string") {
13231
+ const compiledIf = await client.post(
13232
+ `${base}/dialogue-logic/compile`,
13233
+ {
13234
+ code: option.if,
13235
+ target: 0,
13236
+ dialogue: { primaryLinkedValueId: linkedValueId },
13237
+ node: { primaryLinkedValueId: linkedValueId }
13238
+ }
13239
+ );
13240
+ optionSettings = {
13241
+ selectableConditions: [
13242
+ { type: 1, code: option.if, getter: compiledIf.getter }
13243
+ ]
13244
+ };
12324
13245
  }
13246
+ options.push({
13247
+ id: optionId,
13248
+ name: option.label,
13249
+ text: optionTextId,
13250
+ toNodeId: resolveTo(option.to),
13251
+ variables: null,
13252
+ ...optionSettings === null ? {} : { settings: optionSettings }
13253
+ });
13254
+ pendingTexts.push({
13255
+ textId: optionTextId,
13256
+ value: option.text,
13257
+ nodeId,
13258
+ choiceId: optionId
13259
+ });
12325
13260
  }
13261
+ node.optionSettings = { options };
13262
+ } else if (resolveTo(rawNode.to) !== void 0) {
13263
+ node.toNodeId = resolveTo(rawNode.to);
12326
13264
  }
12327
- throw new Error(`Record "${recordKind}:${recordId}" was not found.`);
13265
+ nodes[nodeId] = node;
12328
13266
  }
12329
- if (subcommand === "query") {
12330
- const result = [];
12331
- for (const head of heads.values()) {
12332
- if (filterKind !== null && head.recordKind !== filterKind) continue;
12333
- result.push({ ...head });
12334
- }
12335
- console.log(JSON.stringify(result, null, 2));
12336
- return;
13267
+ const group = isObjectRecord2(spec.group) ? spec.group : null;
13268
+ const triggerConditions = [];
13269
+ if (typeof spec.when === "string") {
13270
+ const compiled = await client.post(
13271
+ `${base}/dialogue-logic/compile`,
13272
+ {
13273
+ code: spec.when,
13274
+ target: 0,
13275
+ dialogue: {
13276
+ primaryLinkedValueId: linkedValueId,
13277
+ triggerNode: {
13278
+ primaryLinkedValueId: linkedValueId,
13279
+ dialogueGroupSettings: group === null ? null : { lookupValueId: linkedValueId }
13280
+ }
13281
+ }
13282
+ }
13283
+ );
13284
+ triggerConditions.push({
13285
+ type: 1,
13286
+ code: spec.when,
13287
+ getter: compiled.getter
13288
+ });
12337
13289
  }
12338
- throw new Error(`Unknown records subcommand "${subcommand}".`);
12339
- }
12340
- async function runValues(context, subcommand, positional, file) {
12341
- if (subcommand === "get" || subcommand === "list") {
12342
- const { raw } = await fetchProjectDocument(context.workspace);
12343
- const values = Array.isArray(raw.values) ? raw.values.filter(isObjectRecord2) : [];
12344
- if (subcommand === "get") {
12345
- const valueId = positional[0];
12346
- if (valueId === void 0)
12347
- throw new Error("Usage: neo values get <valueId>");
12348
- const value = values.find((entry) => entry.id === valueId);
12349
- if (value === void 0)
12350
- throw new Error(`Value "${valueId}" was not found.`);
12351
- console.log(JSON.stringify(value, null, 2));
12352
- return;
12353
- }
12354
- const attributeId = positional[0] ?? null;
12355
- const filtered = attributeId === null ? values : values.filter((entry) => entry.attributeId === attributeId);
13290
+ const trigger = {
13291
+ id: randomUUID4(),
13292
+ type: 0,
13293
+ name: null,
13294
+ layout: { x: 0, y: 0, width: 320, height: 160, isOpen: true },
13295
+ linkedValues: [{ source: 3, valueId: linkedValueId }],
13296
+ primaryLinkedValueId: linkedValueId,
13297
+ conditions: triggerConditions,
13298
+ dialogueGroupSettings: group === null ? null : {
13299
+ dialogueGroupId: group.id,
13300
+ lookupValueId: linkedValueId,
13301
+ priority: {
13302
+ priorityTypeId: group.priority,
13303
+ relativeOrder: typeof group.relativeOrder === "number" ? group.relativeOrder : 0
13304
+ }
13305
+ },
13306
+ occurrenceLimitSettings: group !== null && typeof group.occurrenceLimit === "number" ? { count: group.occurrenceLimit } : null,
13307
+ toNodeId: ids[spec.start]
13308
+ };
13309
+ const body = {
13310
+ name: spec.name,
13311
+ description: typeof spec.description === "string" ? spec.description : null,
13312
+ nodes,
13313
+ linkedValues: [{ source: 3, valueId: linkedValueId }],
13314
+ primaryLinkedValueId: linkedValueId,
13315
+ settings: { defaultSaveOptionChoices: true },
13316
+ triggerNode: trigger
13317
+ };
13318
+ if (dryRun) {
12356
13319
  console.log(
12357
- JSON.stringify(
12358
- filtered.map((entry) => ({
12359
- id: entry.id,
12360
- attributeId: entry.attributeId,
12361
- value: entry.value
12362
- })),
12363
- null,
12364
- 2
12365
- )
13320
+ `Would create "${spec.name}": ${Object.keys(nodes).length} node(s), ${pendingTexts.length} string(s).`
12366
13321
  );
12367
13322
  return;
12368
13323
  }
12369
- if (subcommand === "set") {
12370
- const batch = readBatch(file);
12371
- const edits = [];
12372
- if (batch !== null) {
12373
- for (const entry of batch) {
12374
- if (!isObjectRecord2(entry) || typeof entry.valueId !== "string") {
12375
- throw new Error(
12376
- 'values set batch entries need {"valueId", "value"}.'
12377
- );
12378
- }
12379
- edits.push({ valueId: entry.valueId, value: entry.value });
12380
- }
12381
- } else {
12382
- const [valueId, valueJson] = positional;
12383
- if (valueId === void 0 || valueJson === void 0) {
12384
- throw new Error(
12385
- "Usage: neo values set <valueId> <valueJson> (or batch via stdin/--file)"
12386
- );
12387
- }
12388
- edits.push({ valueId, value: JSON.parse(valueJson) });
12389
- }
12390
- const { heads, raw } = await fetchProjectDocument(context.workspace);
12391
- const valueById = new Map(
12392
- (Array.isArray(raw.values) ? raw.values.filter(isObjectRecord2) : []).map(
12393
- (value) => [String(value.id), value]
12394
- )
12395
- );
12396
- const now = Date.now();
12397
- const convex = await createConvexClient(context.workspace);
12398
- await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
12399
- projectId: context.workspace.config.projectId,
12400
- versionId: context.workspace.config.versionId,
12401
- operation: "update",
12402
- changes: edits.map((edit) => {
12403
- const record = valueById.get(edit.valueId);
12404
- if (record === void 0) {
12405
- throw new Error(`Value "${edit.valueId}" was not found.`);
12406
- }
12407
- return {
12408
- recordKind: "value",
12409
- recordId: edit.valueId,
13324
+ const created = await client.post(
13325
+ `${base}/dialogues/create`,
13326
+ body
13327
+ );
13328
+ const dialogue = isObjectRecord2(created.dialogue) ? created.dialogue : created;
13329
+ const dialogueId = String(dialogue.id);
13330
+ const convex = await createConvexClient(workspace);
13331
+ const now = Date.now();
13332
+ await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
13333
+ projectId: workspace.config.projectId,
13334
+ versionId: workspace.config.versionId,
13335
+ operation: "create",
13336
+ changes: pendingTexts.map((text) => ({
13337
+ recordKind: "localized-text",
13338
+ recordId: text.textId,
13339
+ operation: "create",
13340
+ nextData: {
13341
+ id: text.textId,
13342
+ projectId: workspace.config.projectId,
13343
+ sourceComment: null,
13344
+ links: [
13345
+ text.choiceId === null ? {
13346
+ kind: "dialogue-node-text",
13347
+ recordKind: "dialogue-node",
13348
+ recordId: text.nodeId,
13349
+ nodeId: text.nodeId,
13350
+ fieldPath: "text"
13351
+ } : {
13352
+ kind: "dialogue-choice-text",
13353
+ recordKind: "dialogue-node",
13354
+ recordId: text.nodeId,
13355
+ nodeId: text.nodeId,
13356
+ choiceId: text.choiceId,
13357
+ fieldPath: `optionSettings.options.${text.choiceId}.text`
13358
+ }
13359
+ ],
13360
+ localeValues: {
13361
+ "en-US": {
13362
+ value: text.value,
13363
+ statusId: "localization-status-approved",
13364
+ localeComment: null,
13365
+ updatedAt: now
13366
+ }
13367
+ },
13368
+ createdAt: now,
13369
+ updatedAt: now
13370
+ },
13371
+ intent: createProjectVersionIntent("localized-text.create", {
13372
+ source: "neo-cli-dialogue-apply"
13373
+ }),
13374
+ expectedBaseContentHash: null
13375
+ })),
13376
+ summary: `neo dialogue apply: ${spec.name}`
13377
+ });
13378
+ success(
13379
+ `Created "${spec.name}" (${dialogueId}) \u2014 ${Object.keys(nodes).length} node(s), ${pendingTexts.length} string(s).`
13380
+ );
13381
+ console.log(`${sym.arrow} validate it: neo dialogue dryrun ${dialogueId}`);
13382
+ }
13383
+ var init_dialogue_spec = __esm({
13384
+ "src/commands/dialogue-spec.ts"() {
13385
+ "use strict";
13386
+ init_document();
13387
+ init_projection();
13388
+ init_convex();
13389
+ init_project_version_intents();
13390
+ init_http();
13391
+ init_ui();
13392
+ }
13393
+ });
13394
+
13395
+ // src/commands/dialogue-dryrun.ts
13396
+ var dialogue_dryrun_exports = {};
13397
+ __export(dialogue_dryrun_exports, {
13398
+ runDialogueDryrun: () => runDialogueDryrun
13399
+ });
13400
+ async function runDialogueDryrun(workspace, ref, json) {
13401
+ const { raw } = await fetchProjectDocument(workspace);
13402
+ const document = readDocumentArrays(raw);
13403
+ const world = buildWorld(document, raw);
13404
+ const dialogues = (Array.isArray(raw.dialogues) ? raw.dialogues.filter(isObjectRecord2) : []).filter(
13405
+ (dialogue) => ref === null || dialogue.id === ref || dialogue.name === ref
13406
+ );
13407
+ if (dialogues.length === 0) {
13408
+ throw new Error(
13409
+ ref === null ? "The project has no dialogues." : `No dialogue matches "${ref}".`
13410
+ );
13411
+ }
13412
+ const reports = dialogues.map((dialogue) => dryrunDialogue(dialogue, world));
13413
+ if (json) {
13414
+ console.log(JSON.stringify(reports, null, 2));
13415
+ } else {
13416
+ for (const report of reports) printReport(report);
13417
+ const failed = reports.filter((report) => report.violations.length > 0);
13418
+ console.log("");
13419
+ if (failed.length === 0) {
13420
+ console.log(
13421
+ `${sym.ok} ${reports.length} dialogue(s) dry-ran clean from a fresh save.`
13422
+ );
13423
+ } else {
13424
+ console.log(
13425
+ `${sym.fail} ${failed.length}/${reports.length} dialogue(s) have violations.`
13426
+ );
13427
+ }
13428
+ }
13429
+ if (reports.some((report) => report.violations.length > 0)) {
13430
+ process.exitCode = 1;
13431
+ }
13432
+ }
13433
+ function buildWorld(document, raw) {
13434
+ const valueById = new Map(
13435
+ document.values.map((value) => [String(value.id), value])
13436
+ );
13437
+ const attributeById = new Map(
13438
+ document.attributes.map((attribute) => [String(attribute.id), attribute])
13439
+ );
13440
+ const reachable = (rootContainerId) => {
13441
+ const seen = /* @__PURE__ */ new Set();
13442
+ const queue = rootContainerId === null ? [] : [rootContainerId];
13443
+ while (queue.length > 0) {
13444
+ const id = queue.pop();
13445
+ if (id === void 0 || seen.has(id)) continue;
13446
+ const record = valueById.get(id);
13447
+ if (record === void 0) continue;
13448
+ seen.add(id);
13449
+ const value = record.value;
13450
+ const children = Array.isArray(value) ? value : isObjectRecord2(value) ? Object.values(value) : [];
13451
+ for (const child of children) {
13452
+ if (typeof child === "string" && valueById.has(child)) {
13453
+ queue.push(child);
13454
+ }
13455
+ }
13456
+ }
13457
+ return seen;
13458
+ };
13459
+ let saveOwned = /* @__PURE__ */ new Set();
13460
+ let sessionOwned = /* @__PURE__ */ new Set();
13461
+ for (const { root, attributeId } of projectRootAttributesInDisplayOrder(
13462
+ document.project
13463
+ )) {
13464
+ const attribute = attributeById.get(attributeId);
13465
+ const containerId = attribute !== void 0 && typeof attribute.valueId === "string" ? attribute.valueId : null;
13466
+ if (root.key === "Save") saveOwned = reachable(containerId);
13467
+ if (root.key === "Session") sessionOwned = reachable(containerId);
13468
+ }
13469
+ const locById = /* @__PURE__ */ new Map();
13470
+ const texts = Array.isArray(raw.localizedTexts) ? raw.localizedTexts.filter(isObjectRecord2) : [];
13471
+ for (const text of texts) {
13472
+ const locales = isObjectRecord2(text.localeValues) ? text.localeValues : {};
13473
+ const main2 = isObjectRecord2(locales["en-US"]) ? locales["en-US"] : {};
13474
+ locById.set(
13475
+ String(text.id),
13476
+ typeof main2.value === "string" ? main2.value : null
13477
+ );
13478
+ }
13479
+ const lookupKeys = /* @__PURE__ */ new Set();
13480
+ const lookupAttributeIds = new Set(
13481
+ document.attributes.filter((attribute) => attribute.type === 9).map((attribute) => String(attribute.id))
13482
+ );
13483
+ for (const type of document.types) {
13484
+ if (!isObjectRecord2(type.schema)) continue;
13485
+ for (const [key, attributeId] of Object.entries(type.schema)) {
13486
+ if (typeof attributeId === "string" && lookupAttributeIds.has(attributeId)) {
13487
+ lookupKeys.add(key);
13488
+ }
13489
+ }
13490
+ }
13491
+ return {
13492
+ valueById,
13493
+ rootValue: buildRootValue(document),
13494
+ vm: {
13495
+ project: document.project,
13496
+ attributes: document.attributes,
13497
+ values: document.values,
13498
+ types: document.types,
13499
+ enums: document.enums,
13500
+ projectFiles: document.projectFiles,
13501
+ localizationConfig: document.localizationConfig,
13502
+ localizedTexts: document.localizedTexts,
13503
+ databaseVM: makeEvaluatorLookups(
13504
+ document.attributes,
13505
+ document.values
13506
+ )
13507
+ },
13508
+ saveOwned,
13509
+ sessionOwned,
13510
+ locById,
13511
+ lookupKeys
13512
+ };
13513
+ }
13514
+ function dryrunDialogue(dialogue, world) {
13515
+ const name = String(dialogue.name ?? dialogue.id);
13516
+ const nodes = isObjectRecord2(dialogue.nodes) ? dialogue.nodes : {};
13517
+ const report = {
13518
+ dialogue: name,
13519
+ dialogueId: String(dialogue.id),
13520
+ nodesVisited: 0,
13521
+ nodesTotal: Object.keys(nodes).length,
13522
+ pathsWalked: 0,
13523
+ endings: 0,
13524
+ actionsEvaluated: 0,
13525
+ violations: [],
13526
+ warnings: []
13527
+ };
13528
+ const violate = (node, message) => {
13529
+ report.violations.push({ dialogue: name, node, message });
13530
+ };
13531
+ const trigger = isObjectRecord2(dialogue.triggerNode) ? dialogue.triggerNode : null;
13532
+ const start = trigger?.toNodeId;
13533
+ if (typeof start !== "string") {
13534
+ report.warnings.push("no trigger entry point \u2014 dialogue can never fire");
13535
+ return report;
13536
+ }
13537
+ const visited = /* @__PURE__ */ new Set();
13538
+ const evaluatedActionNodes = /* @__PURE__ */ new Set();
13539
+ const walk = (nodeId, pathSeen) => {
13540
+ const node = nodes[nodeId];
13541
+ if (node === void 0) {
13542
+ violate(nodeId.slice(0, 8), "edge points at a node that does not exist");
13543
+ return;
13544
+ }
13545
+ if (pathSeen.has(nodeId)) return;
13546
+ const nextSeen = new Set(pathSeen);
13547
+ nextSeen.add(nodeId);
13548
+ visited.add(nodeId);
13549
+ const label = String(node.name ?? nodeId.slice(0, 8));
13550
+ if (typeof node.text === "string") {
13551
+ const value = world.locById.get(node.text);
13552
+ if (value === void 0) violate(label, "node text record is missing");
13553
+ else if (value === null || value === "")
13554
+ violate(label, "node text has no en-US value");
13555
+ }
13556
+ if (node.type === 2 && !evaluatedActionNodes.has(nodeId)) {
13557
+ evaluatedActionNodes.add(nodeId);
13558
+ runActions(node, label, world, report, violate);
13559
+ }
13560
+ const edges = [];
13561
+ if (typeof node.toNodeId === "string") edges.push(node.toNodeId);
13562
+ const optionSettings = isObjectRecord2(node.optionSettings) ? node.optionSettings : null;
13563
+ const options = Array.isArray(optionSettings?.options) ? optionSettings.options.filter(isObjectRecord2) : [];
13564
+ for (const option of options) {
13565
+ const text = typeof option.text === "string" ? world.locById.get(option.text) : void 0;
13566
+ if (text === void 0)
13567
+ violate(label, `option "${String(option.name)}" text record missing`);
13568
+ else if (text === null || text === "")
13569
+ violate(label, `option "${String(option.name)}" has no en-US value`);
13570
+ if (typeof option.toNodeId === "string") edges.push(option.toNodeId);
13571
+ else violate(label, `option "${String(option.name)}" leads nowhere`);
13572
+ }
13573
+ const outcomes = Array.isArray(node.outcomes) ? node.outcomes.filter(isObjectRecord2) : [];
13574
+ for (const outcome of outcomes) {
13575
+ runOutcomeConditions(node, outcome, label, world, report, violate);
13576
+ if (typeof outcome.toNodeId === "string") edges.push(outcome.toNodeId);
13577
+ }
13578
+ if (edges.length === 0) {
13579
+ report.pathsWalked += 1;
13580
+ report.endings += 1;
13581
+ return;
13582
+ }
13583
+ for (const edge of edges) walk(edge, nextSeen);
13584
+ };
13585
+ walk(start, /* @__PURE__ */ new Set());
13586
+ report.nodesVisited = visited.size;
13587
+ for (const [nodeId, node] of Object.entries(nodes)) {
13588
+ if (!visited.has(nodeId)) {
13589
+ report.warnings.push(
13590
+ `unreachable node "${String(node.name ?? nodeId.slice(0, 8))}"`
13591
+ );
13592
+ }
13593
+ }
13594
+ return report;
13595
+ }
13596
+ function evaluatorContext(node, world) {
13597
+ const primary = typeof node.primaryLinkedValueId === "string" ? node.primaryLinkedValueId : null;
13598
+ const thisRecord = primary === null ? null : world.valueById.get(primary);
13599
+ return {
13600
+ vm: world.vm,
13601
+ thisValue: thisRecord?.value ?? null,
13602
+ rootValue: world.rootValue,
13603
+ dialogueContext: null
13604
+ };
13605
+ }
13606
+ function runActions(node, label, world, report, violate) {
13607
+ const actions = Array.isArray(node.actions) ? node.actions.filter(isObjectRecord2) : [];
13608
+ const ctx = evaluatorContext(node, world);
13609
+ for (const action of actions) {
13610
+ const logic = isObjectRecord2(action.logic) ? action.logic : null;
13611
+ const compiled = logic !== null ? logic.action : void 0;
13612
+ if (!isObjectRecord2(compiled)) {
13613
+ report.warnings.push(
13614
+ `"${label}" has an action with no compiled logic (no-op)`
13615
+ );
13616
+ continue;
13617
+ }
13618
+ report.actionsEvaluated += 1;
13619
+ checkLookupMutationArgs(compiled, label, world, violate);
13620
+ let writes;
13621
+ try {
13622
+ writes = evaluateNSAction(compiled, ctx).writes;
13623
+ } catch (error) {
13624
+ const message = error instanceof Error ? error.message : String(error);
13625
+ if (message.includes("no native delegate is available")) {
13626
+ report.warnings.push(
13627
+ `"${label}" calls a native function \u2014 skipped in simulation (${message.split("'")[1] ?? "native"})`
13628
+ );
13629
+ } else {
13630
+ violate(label, `action failed to evaluate: ${message}`);
13631
+ }
13632
+ continue;
13633
+ }
13634
+ for (const write of writes) {
13635
+ checkWriteOwnership(write, compiled, label, ctx, world, violate);
13636
+ }
13637
+ }
13638
+ }
13639
+ function runOutcomeConditions(node, outcome, label, world, report, violate) {
13640
+ const conditions = Array.isArray(outcome.conditions) ? outcome.conditions.filter(isObjectRecord2) : [];
13641
+ const ctx = evaluatorContext(node, world);
13642
+ for (const condition of conditions) {
13643
+ if (!isObjectRecord2(condition.getter)) continue;
13644
+ report.actionsEvaluated += 1;
13645
+ try {
13646
+ const value = evaluateNSGetter(condition.getter, ctx);
13647
+ if (typeof value !== "boolean") {
13648
+ violate(label, `outcome condition returned ${typeof value}, not bool`);
13649
+ }
13650
+ } catch (error) {
13651
+ violate(
13652
+ label,
13653
+ `outcome condition failed: ${error instanceof Error ? error.message : String(error)}`
13654
+ );
13655
+ }
13656
+ }
13657
+ }
13658
+ function checkWriteOwnership(write, compiledAction, label, ctx, world, violate) {
13659
+ const target = isObjectRecord2(write.target) ? write.target : null;
13660
+ if (target === null) {
13661
+ violate(label, `${write.kind} write has no target`);
13662
+ return;
13663
+ }
13664
+ if (write.kind !== "assign") {
13665
+ const literalKey = targetLiteralKey(target);
13666
+ if (literalKey !== null && world.lookupKeys.has(literalKey)) return;
13667
+ }
13668
+ const writability = typeof target.writability === "string" ? target.writability : null;
13669
+ const sessionLike = writability === "session" || writability === "assetToSessionLookup" || writability === "local";
13670
+ const saveLike = writability === "save" || writability === "assetToSaveLookup";
13671
+ if (writability !== null && !saveLike && !sessionLike) {
13672
+ violate(
13673
+ label,
13674
+ `${write.kind} write targets a ${writability} value \u2014 the runtime refuses non save/session mutations`
13675
+ );
13676
+ return;
13677
+ }
13678
+ const rowId = resolveWriteRowId(target, compiledAction, ctx, world);
13679
+ if (rowId === null) {
13680
+ violate(
13681
+ label,
13682
+ `${write.kind} write receiver did not resolve to a stored value row \u2014 null lookup (missing save-map entry?) or a lookup-resolved asset collection; the runtime cannot apply this write`
13683
+ );
13684
+ return;
13685
+ }
13686
+ const owned = sessionLike ? world.sessionOwned.has(rowId) : world.saveOwned.has(rowId);
13687
+ if (!owned) {
13688
+ violate(
13689
+ label,
13690
+ `${write.kind} write lands on '${rowId}' which is not ${sessionLike ? "session" : "save"}-owned \u2014 the runtime will throw exactly here`
13691
+ );
13692
+ }
13693
+ }
13694
+ function checkLookupMutationArgs(compiledAction, label, world, violate) {
13695
+ const scan = (value) => {
13696
+ if (Array.isArray(value)) {
13697
+ for (const entry of value) scan(entry);
13698
+ return;
13699
+ }
13700
+ if (!isObjectRecord2(value)) return;
13701
+ const mutation = value.mutation;
13702
+ const target = isObjectRecord2(value.target) ? value.target : null;
13703
+ if (typeof mutation === "string" && target !== null) {
13704
+ const literalKey = targetLiteralKey(target);
13705
+ if (literalKey !== null && world.lookupKeys.has(literalKey)) {
13706
+ const args = Array.isArray(value.args) ? value.args : [];
13707
+ for (const arg of args) {
13708
+ if (isObjectRecord2(arg) && arg.type === "value" && isObjectRecord2(arg.value) && typeof arg.value.value === "string") {
13709
+ violate(
13710
+ label,
13711
+ `${mutation} on lookup "${literalKey}" passes an id string literal \u2014 the runtime needs a collection entry (bind the node to the item and use Add(this))`
13712
+ );
13713
+ }
13714
+ }
13715
+ }
13716
+ }
13717
+ for (const child of Object.values(value)) scan(child);
13718
+ };
13719
+ scan(compiledAction.instructions);
13720
+ }
13721
+ function targetLiteralKey(target) {
13722
+ const pointer = isObjectRecord2(target.pointer) ? target.pointer : target;
13723
+ if (pointer.type !== "keyOf" || !isObjectRecord2(pointer.keyOf)) return null;
13724
+ const key = pointer.keyOf.key;
13725
+ if (!isObjectRecord2(key) || !isObjectRecord2(key.value)) return null;
13726
+ return typeof key.value.value === "string" ? key.value.value : null;
13727
+ }
13728
+ function resolveWriteRowId(target, compiledAction, ctx, world) {
13729
+ const pointer = isObjectRecord2(target.pointer) ? target.pointer : target;
13730
+ const evalPointer2 = (p) => {
13731
+ const getter = {
13732
+ parameters: compiledAction.parameters ?? [],
13733
+ instructions: [{ type: "return", pointer: p }],
13734
+ typeInfo: { type: 0, required: false }
13735
+ };
13736
+ try {
13737
+ return evaluateNSGetter(getter, ctx);
13738
+ } catch {
13739
+ return void 0;
13740
+ }
13741
+ };
13742
+ if (pointer.type === "keyOf" && isObjectRecord2(pointer.keyOf)) {
13743
+ const keyOf = pointer.keyOf;
13744
+ const key = isObjectRecord2(keyOf.key) ? keyOf.key : null;
13745
+ const literalKey = key !== null && isObjectRecord2(key.value) ? key.value.value : void 0;
13746
+ const receiver2 = evalPointer2(keyOf.pointer);
13747
+ if (isObjectRecord2(receiver2) && typeof literalKey === "string") {
13748
+ const child = receiver2[literalKey];
13749
+ if (typeof child === "string" && world.valueById.has(child)) {
13750
+ return child;
13751
+ }
13752
+ }
13753
+ return null;
13754
+ }
13755
+ if (pointer.type === "reference" && typeof pointer.valueId === "string") {
13756
+ return pointer.valueId;
13757
+ }
13758
+ const receiver = evalPointer2(pointer);
13759
+ if (receiver !== void 0 && receiver !== null) {
13760
+ for (const [id, record] of world.valueById) {
13761
+ if (record.value === receiver) return id;
13762
+ }
13763
+ }
13764
+ return null;
13765
+ }
13766
+ function printReport(report) {
13767
+ const headline = `${report.dialogue} ${color.dim(
13768
+ `${report.nodesVisited}/${report.nodesTotal} nodes, ${report.endings} ending(s), ${report.actionsEvaluated} action/condition eval(s)`
13769
+ )}`;
13770
+ if (report.violations.length === 0) {
13771
+ console.log(`${sym.ok} ${headline}`);
13772
+ } else {
13773
+ console.log(`${sym.fail} ${headline}`);
13774
+ for (const violation of report.violations) {
13775
+ console.log(` ${color.red(violation.node)}: ${violation.message}`);
13776
+ }
13777
+ }
13778
+ for (const warning of report.warnings) {
13779
+ console.log(` ${sym.warn} ${color.dim(warning)}`);
13780
+ }
13781
+ }
13782
+ var init_dialogue_dryrun = __esm({
13783
+ "src/commands/dialogue-dryrun.ts"() {
13784
+ "use strict";
13785
+ init_document();
13786
+ init_projection();
13787
+ init_neoscript_evaluator();
13788
+ init_project_root_attributes();
13789
+ init_ui();
13790
+ init_script();
13791
+ }
13792
+ });
13793
+
13794
+ // src/commands/content.ts
13795
+ var content_exports = {};
13796
+ __export(content_exports, {
13797
+ makeContentContext: () => makeContentContext,
13798
+ runDialogue: () => runDialogue,
13799
+ runFiles: () => runFiles,
13800
+ runLoc: () => runLoc,
13801
+ runRecords: () => runRecords,
13802
+ runValues: () => runValues
13803
+ });
13804
+ import { readFileSync as readFileSync10 } from "node:fs";
13805
+ import { randomUUID as randomUUID5 } from "node:crypto";
13806
+ function versionPath(workspace, suffix) {
13807
+ return `/api/projects/${workspace.config.projectId}/versions/${workspace.config.versionId}/${suffix}`;
13808
+ }
13809
+ function readBatch(file) {
13810
+ let raw = null;
13811
+ if (file !== null) {
13812
+ raw = readFileSync10(file, "utf8");
13813
+ } else if (!process.stdin.isTTY) {
13814
+ raw = readFileSync10(0, "utf8");
13815
+ if (raw.trim().length === 0) raw = null;
13816
+ }
13817
+ if (raw === null) return null;
13818
+ const parsed = JSON.parse(raw);
13819
+ if (!Array.isArray(parsed)) {
13820
+ throw new Error("Batch input must be a JSON array.");
13821
+ }
13822
+ return parsed;
13823
+ }
13824
+ async function runRecords(context, subcommand, positional, filterKind) {
13825
+ const { raw, records, heads } = await fetchProjectDocument(context.workspace);
13826
+ if (subcommand === "get") {
13827
+ const [recordKind, recordId] = positional;
13828
+ if (recordKind === void 0 || recordId === void 0) {
13829
+ throw new Error("Usage: neo records get <recordKind> <recordId>");
13830
+ }
13831
+ const fromMap = records.get(`${recordKind}:${recordId}`);
13832
+ if (fromMap !== void 0) {
13833
+ console.log(JSON.stringify(fromMap, null, 2));
13834
+ return;
13835
+ }
13836
+ for (const value of Object.values(raw)) {
13837
+ if (!Array.isArray(value)) continue;
13838
+ for (const entry of value) {
13839
+ if (isObjectRecord2(entry) && entry.id === recordId) {
13840
+ console.log(JSON.stringify(entry, null, 2));
13841
+ return;
13842
+ }
13843
+ }
13844
+ }
13845
+ throw new Error(`Record "${recordKind}:${recordId}" was not found.`);
13846
+ }
13847
+ if (subcommand === "query") {
13848
+ const result = [];
13849
+ for (const head of heads.values()) {
13850
+ if (filterKind !== null && head.recordKind !== filterKind) continue;
13851
+ result.push({ ...head });
13852
+ }
13853
+ console.log(JSON.stringify(result, null, 2));
13854
+ return;
13855
+ }
13856
+ throw new Error(`Unknown records subcommand "${subcommand}".`);
13857
+ }
13858
+ async function runValues(context, subcommand, positional, file) {
13859
+ if (subcommand === "get" || subcommand === "list") {
13860
+ const { raw } = await fetchProjectDocument(context.workspace);
13861
+ const values = Array.isArray(raw.values) ? raw.values.filter(isObjectRecord2) : [];
13862
+ if (subcommand === "get") {
13863
+ const valueId = positional[0];
13864
+ if (valueId === void 0)
13865
+ throw new Error("Usage: neo values get <valueId>");
13866
+ const value = values.find((entry) => entry.id === valueId);
13867
+ if (value === void 0)
13868
+ throw new Error(`Value "${valueId}" was not found.`);
13869
+ console.log(JSON.stringify(value, null, 2));
13870
+ return;
13871
+ }
13872
+ const attributeId = positional[0] ?? null;
13873
+ const filtered = attributeId === null ? values : values.filter((entry) => entry.attributeId === attributeId);
13874
+ console.log(
13875
+ JSON.stringify(
13876
+ filtered.map((entry) => ({
13877
+ id: entry.id,
13878
+ attributeId: entry.attributeId,
13879
+ value: entry.value
13880
+ })),
13881
+ null,
13882
+ 2
13883
+ )
13884
+ );
13885
+ return;
13886
+ }
13887
+ if (subcommand === "add-entry" || subcommand === "delete-entry") {
13888
+ const containerValueId = positional[0];
13889
+ const jsonArg = positional[1];
13890
+ if (containerValueId === void 0 || jsonArg === void 0) {
13891
+ throw new Error(
13892
+ `Usage: neo values ${subcommand} <containerValueId> '<json>'`
13893
+ );
13894
+ }
13895
+ const verb = subcommand === "add-entry" ? "add" : "delete";
13896
+ const result = await context.client.post(
13897
+ versionPath(
13898
+ context.workspace,
13899
+ `values/${containerValueId}/entries/${verb}`
13900
+ ),
13901
+ JSON.parse(jsonArg)
13902
+ );
13903
+ console.log(JSON.stringify(result, null, 2));
13904
+ return;
13905
+ }
13906
+ if (subcommand === "create") {
13907
+ const jsonArg = positional[0];
13908
+ if (jsonArg === void 0) {
13909
+ throw new Error(
13910
+ "Usage: neo values create '<valueJson>' [--into <containerValueId> --key <AttributeName>]"
13911
+ );
13912
+ }
13913
+ const intoIndex = process.argv.indexOf("--into");
13914
+ const keyIndex = process.argv.indexOf("--key");
13915
+ const containerValueId = intoIndex >= 0 ? process.argv[intoIndex + 1] ?? null : null;
13916
+ const containerKey = keyIndex >= 0 ? process.argv[keyIndex + 1] ?? null : null;
13917
+ if (containerValueId === null !== (containerKey === null)) {
13918
+ throw new Error(
13919
+ "values create needs BOTH --into and --key (or neither)."
13920
+ );
13921
+ }
13922
+ const newValueId = randomUUID5();
13923
+ const { heads, raw } = await fetchProjectDocument(context.workspace);
13924
+ const changes = [
13925
+ {
13926
+ recordKind: "value",
13927
+ recordId: newValueId,
13928
+ operation: "create",
13929
+ // Full IAttributeValueProps shape — the export read bundle validates
13930
+ // every value record (id/projectId/value/createdAt/updatedAt).
13931
+ nextData: {
13932
+ id: newValueId,
13933
+ projectId: context.workspace.config.projectId,
13934
+ value: JSON.parse(jsonArg),
13935
+ createdAt: Date.now(),
13936
+ updatedAt: Date.now()
13937
+ },
13938
+ intent: createProjectVersionIntent("value.create", {
13939
+ source: "neo-cli-values"
13940
+ }),
13941
+ expectedBaseContentHash: null
13942
+ }
13943
+ ];
13944
+ if (containerValueId !== null && containerKey !== null) {
13945
+ const values = Array.isArray(raw.values) ? raw.values.filter(isObjectRecord2) : [];
13946
+ const container = values.find((value) => value.id === containerValueId);
13947
+ if (container === void 0) {
13948
+ throw new Error(`Container value "${containerValueId}" was not found.`);
13949
+ }
13950
+ if (!isObjectRecord2(container.value)) {
13951
+ throw new Error(
13952
+ `Container value "${containerValueId}" is not an attribute map.`
13953
+ );
13954
+ }
13955
+ changes.push({
13956
+ recordKind: "value",
13957
+ recordId: containerValueId,
13958
+ operation: "update",
13959
+ nextData: {
13960
+ ...container,
13961
+ value: { ...container.value, [containerKey]: newValueId }
13962
+ },
13963
+ intent: createProjectVersionIntent("value.update", {
13964
+ source: "neo-cli-values"
13965
+ }),
13966
+ expectedBaseContentHash: heads.get(`value:${containerValueId}`)?.contentHash ?? null
13967
+ });
13968
+ }
13969
+ const convex = await createConvexClient(context.workspace);
13970
+ await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
13971
+ projectId: context.workspace.config.projectId,
13972
+ versionId: context.workspace.config.versionId,
13973
+ operation: "create",
13974
+ changes,
13975
+ summary: "neo values create",
13976
+ force: true
13977
+ });
13978
+ console.log(newValueId);
13979
+ return;
13980
+ }
13981
+ if (subcommand === "set") {
13982
+ const batch = readBatch(file);
13983
+ const edits = [];
13984
+ if (batch !== null) {
13985
+ for (const entry of batch) {
13986
+ if (!isObjectRecord2(entry) || typeof entry.valueId !== "string") {
13987
+ throw new Error(
13988
+ 'values set batch entries need {"valueId", "value"}.'
13989
+ );
13990
+ }
13991
+ edits.push({ valueId: entry.valueId, value: entry.value });
13992
+ }
13993
+ } else {
13994
+ const [valueId, valueJson] = positional;
13995
+ if (valueId === void 0 || valueJson === void 0) {
13996
+ throw new Error(
13997
+ "Usage: neo values set <valueId> <valueJson> (or batch via stdin/--file)"
13998
+ );
13999
+ }
14000
+ edits.push({ valueId, value: JSON.parse(valueJson) });
14001
+ }
14002
+ const { heads, raw } = await fetchProjectDocument(context.workspace);
14003
+ const valueById = new Map(
14004
+ (Array.isArray(raw.values) ? raw.values.filter(isObjectRecord2) : []).map(
14005
+ (value) => [String(value.id), value]
14006
+ )
14007
+ );
14008
+ const now = Date.now();
14009
+ const convex = await createConvexClient(context.workspace);
14010
+ await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
14011
+ projectId: context.workspace.config.projectId,
14012
+ versionId: context.workspace.config.versionId,
14013
+ operation: "update",
14014
+ changes: edits.map((edit) => {
14015
+ const record = valueById.get(edit.valueId);
14016
+ if (record === void 0) {
14017
+ throw new Error(`Value "${edit.valueId}" was not found.`);
14018
+ }
14019
+ return {
14020
+ recordKind: "value",
14021
+ recordId: edit.valueId,
12410
14022
  operation: "update",
12411
- nextData: { ...record, value: edit.value, updatedAt: now },
14023
+ // Backfill the required record envelope (repairs rows minted by
14024
+ // older CLI builds that wrote only {id, value}).
14025
+ nextData: {
14026
+ projectId: context.workspace.config.projectId,
14027
+ createdAt: now,
14028
+ ...record,
14029
+ value: edit.value,
14030
+ updatedAt: now
14031
+ },
12412
14032
  intent: createProjectVersionIntent("value.update", {
12413
14033
  source: "neo-cli-values"
12414
14034
  }),
@@ -12471,6 +14091,107 @@ async function runLoc(context, subcommand, positional, file) {
12471
14091
  }
12472
14092
  return;
12473
14093
  }
14094
+ if (subcommand === "create") {
14095
+ const jsonArg = positional[0];
14096
+ if (jsonArg === void 0 || !jsonArg.trim().startsWith("{")) {
14097
+ throw new Error(
14098
+ `Usage: neo loc create '{"value":"...","locale":"en-US","links":[...]}'`
14099
+ );
14100
+ }
14101
+ const body = JSON.parse(jsonArg);
14102
+ if (typeof body.value !== "string") {
14103
+ throw new Error('loc create requires a string "value".');
14104
+ }
14105
+ const locale = typeof body.locale === "string" ? body.locale : "en-US";
14106
+ const { heads, raw } = await fetchProjectDocument(context.workspace);
14107
+ const config = isObjectRecord2(raw.localizationConfig) ? raw.localizationConfig : {};
14108
+ const statusId = config.mainLocale === locale && typeof config.mainLocaleDefaultStatusId === "string" ? config.mainLocaleDefaultStatusId : "localization-status-needs-translation";
14109
+ const now = Date.now();
14110
+ const textId = typeof body.id === "string" ? body.id : randomUUID5();
14111
+ const existingHead = heads.get(`localized-text:${textId}`);
14112
+ const text = {
14113
+ id: textId,
14114
+ projectId: context.workspace.config.projectId,
14115
+ sourceComment: typeof body.sourceComment === "string" ? body.sourceComment : null,
14116
+ links: Array.isArray(body.links) ? body.links : [],
14117
+ localeValues: {
14118
+ [locale]: {
14119
+ value: body.value,
14120
+ statusId,
14121
+ localeComment: null,
14122
+ updatedAt: now
14123
+ }
14124
+ },
14125
+ createdAt: now,
14126
+ updatedAt: now
14127
+ };
14128
+ const operation = existingHead === void 0 ? "create" : "update";
14129
+ const convex = await createConvexClient(context.workspace);
14130
+ await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
14131
+ projectId: context.workspace.config.projectId,
14132
+ versionId: context.workspace.config.versionId,
14133
+ operation,
14134
+ changes: [
14135
+ {
14136
+ recordKind: "localized-text",
14137
+ recordId: textId,
14138
+ operation,
14139
+ nextData: text,
14140
+ intent: createProjectVersionIntent(`localized-text.${operation}`, {
14141
+ source: "neo-cli-loc"
14142
+ }),
14143
+ expectedBaseContentHash: existingHead?.contentHash ?? null
14144
+ }
14145
+ ],
14146
+ summary: `neo loc ${operation}`
14147
+ });
14148
+ console.log(textId);
14149
+ return;
14150
+ }
14151
+ if (subcommand === "delete") {
14152
+ const textId = positional[0];
14153
+ if (textId === void 0) {
14154
+ throw new Error("Usage: neo loc delete <textId>");
14155
+ }
14156
+ const { heads } = await fetchProjectDocument(context.workspace);
14157
+ const head = heads.get(`localized-text:${textId}`);
14158
+ if (head === void 0) {
14159
+ throw new Error(`Localized text "${textId}" was not found.`);
14160
+ }
14161
+ const convex = await createConvexClient(context.workspace);
14162
+ await convex.mutation(api.serverProjectRecordOperations.commitFromSession, {
14163
+ projectId: context.workspace.config.projectId,
14164
+ versionId: context.workspace.config.versionId,
14165
+ operation: "delete",
14166
+ changes: [
14167
+ {
14168
+ recordKind: "localized-text",
14169
+ recordId: textId,
14170
+ operation: "delete",
14171
+ deleted: true,
14172
+ intent: createProjectVersionIntent("localized-text.delete", {
14173
+ source: "neo-cli-loc"
14174
+ }),
14175
+ expectedBaseContentHash: head.contentHash
14176
+ }
14177
+ ],
14178
+ summary: "neo loc delete"
14179
+ });
14180
+ console.log(`deleted ${textId}`);
14181
+ return;
14182
+ }
14183
+ if (subcommand === "archive" || subcommand === "restore") {
14184
+ const textId = positional[0];
14185
+ if (textId === void 0) {
14186
+ throw new Error(`Usage: neo loc ${subcommand} <textId>`);
14187
+ }
14188
+ const result = await context.client.post(
14189
+ versionPath(context.workspace, `localized-texts/${textId}/archive/edit`),
14190
+ { archived: subcommand === "archive" }
14191
+ );
14192
+ console.log(JSON.stringify(result, null, 2));
14193
+ return;
14194
+ }
12474
14195
  if (subcommand === "status-set") {
12475
14196
  const [textId, locale, statusId] = positional;
12476
14197
  if (textId === void 0 || locale === void 0 || statusId === void 0) {
@@ -12593,6 +14314,103 @@ async function runDialogue(context, subcommand, positional, file) {
12593
14314
  `Unknown ${subcommand} verb "${String(groupVerb)}" (list | create | edit | delete).`
12594
14315
  );
12595
14316
  }
14317
+ if (subcommand === "create" || subcommand === "edit" || subcommand === "delete") {
14318
+ const readBody = () => {
14319
+ const batch = readBatch(file);
14320
+ if (batch !== null) {
14321
+ throw new Error(
14322
+ `neo dialogue ${subcommand} takes one JSON body, not a batch.`
14323
+ );
14324
+ }
14325
+ const jsonArg = positional[positional.length - 1];
14326
+ if (jsonArg === void 0 || !jsonArg.trim().startsWith("{")) {
14327
+ throw new Error(
14328
+ `Usage: neo dialogue ${subcommand} ${subcommand === "create" ? "'<IDialogueBase json>'" : "<dialogueId> ['<json>']"}${file === null ? " (or --file <path>)" : ""}`
14329
+ );
14330
+ }
14331
+ return JSON.parse(jsonArg);
14332
+ };
14333
+ if (subcommand === "create") {
14334
+ const result2 = await context.client.post(
14335
+ versionPath(context.workspace, "dialogues/create"),
14336
+ readBody()
14337
+ );
14338
+ console.log(JSON.stringify(result2, null, 2));
14339
+ return;
14340
+ }
14341
+ const dialogueId = positional[0];
14342
+ if (dialogueId === void 0) {
14343
+ throw new Error(`Usage: neo dialogue ${subcommand} <dialogueId>`);
14344
+ }
14345
+ const result = await context.client.post(
14346
+ versionPath(context.workspace, `dialogues/${dialogueId}/${subcommand}`),
14347
+ subcommand === "edit" ? readBody() : void 0
14348
+ );
14349
+ console.log(JSON.stringify(result, null, 2));
14350
+ return;
14351
+ }
14352
+ if (subcommand === "export") {
14353
+ const ref = positional[0];
14354
+ if (ref === void 0) {
14355
+ throw new Error("Usage: neo dialogue export <dialogueIdOrName>");
14356
+ }
14357
+ const { runDialogueExport: runDialogueExport2 } = await Promise.resolve().then(() => (init_dialogue_spec(), dialogue_spec_exports));
14358
+ await runDialogueExport2(context.workspace, ref);
14359
+ return;
14360
+ }
14361
+ if (subcommand === "apply") {
14362
+ const specFile = file ?? positional[0];
14363
+ if (specFile === void 0) {
14364
+ throw new Error(
14365
+ "Usage: neo dialogue apply <spec.json> [--dry-run] (or --file <path>)"
14366
+ );
14367
+ }
14368
+ const { runDialogueApply: runDialogueApply2 } = await Promise.resolve().then(() => (init_dialogue_spec(), dialogue_spec_exports));
14369
+ await runDialogueApply2(
14370
+ context.workspace,
14371
+ specFile,
14372
+ process.argv.includes("--dry-run")
14373
+ );
14374
+ return;
14375
+ }
14376
+ if (subcommand === "dryrun") {
14377
+ const { runDialogueDryrun: runDialogueDryrun2 } = await Promise.resolve().then(() => (init_dialogue_dryrun(), dialogue_dryrun_exports));
14378
+ await runDialogueDryrun2(
14379
+ context.workspace,
14380
+ positional[0] ?? null,
14381
+ context.json
14382
+ );
14383
+ return;
14384
+ }
14385
+ if (subcommand === "compile") {
14386
+ const jsonArg = positional[0];
14387
+ if (jsonArg === void 0) {
14388
+ throw new Error(
14389
+ `Usage: neo dialogue compile '{"code":"...","target":0|1|2,"node":{"primaryLinkedValueId":...}}'`
14390
+ );
14391
+ }
14392
+ const result = await context.client.post(
14393
+ versionPath(context.workspace, "dialogue-logic/compile"),
14394
+ JSON.parse(jsonArg)
14395
+ );
14396
+ console.log(JSON.stringify(result, null, 2));
14397
+ return;
14398
+ }
14399
+ if (subcommand === "text") {
14400
+ const dialogueId = positional[0];
14401
+ const jsonArg = positional[1];
14402
+ if (dialogueId === void 0 || jsonArg === void 0) {
14403
+ throw new Error(
14404
+ `Usage: neo dialogue text <dialogueId> '{"nodeId":...,"textId":...,"locale":"en-US","value":...,"variableIds":[]}'`
14405
+ );
14406
+ }
14407
+ const result = await context.client.post(
14408
+ versionPath(context.workspace, `dialogues/${dialogueId}/rich-text/edit`),
14409
+ JSON.parse(jsonArg)
14410
+ );
14411
+ console.log(JSON.stringify(result, null, 2));
14412
+ return;
14413
+ }
12596
14414
  if (subcommand === "node") {
12597
14415
  const nodeVerb = positional[0];
12598
14416
  const dialogueId = positional[1];
@@ -12641,10 +14459,110 @@ async function runDialogue(context, subcommand, positional, file) {
12641
14459
  );
12642
14460
  }
12643
14461
  throw new Error(
12644
- `Unknown dialogue subcommand "${subcommand}" (list | show | node | group | priority-group).`
14462
+ `Unknown dialogue subcommand "${subcommand}" (list | show | export | apply | dryrun | create | edit | delete | text | compile | node | group | priority-group).`
14463
+ );
14464
+ }
14465
+ async function uploadProjectFile(context, filePath, preferredTemplateId) {
14466
+ const { readFileSync: readFile } = await import("node:fs");
14467
+ const { basename, extname } = await import("node:path");
14468
+ const bytes = readFile(filePath);
14469
+ const name = basename(filePath);
14470
+ const extension = extname(filePath).toLowerCase();
14471
+ const mimeByExtension = {
14472
+ ".png": "image/png",
14473
+ ".jpg": "image/jpeg",
14474
+ ".jpeg": "image/jpeg",
14475
+ ".gif": "image/gif",
14476
+ ".wav": "audio/wav",
14477
+ ".mp3": "audio/mpeg",
14478
+ ".ogg": "audio/ogg"
14479
+ };
14480
+ const mimeType = mimeByExtension[extension];
14481
+ if (mimeType === void 0) {
14482
+ throw new Error(
14483
+ `Unsupported upload extension "${extension}" (png/jpg/gif/wav/mp3/ogg).`
14484
+ );
14485
+ }
14486
+ const replaceIndex = process.argv.indexOf("--replace");
14487
+ const replaceFileId = replaceIndex >= 0 ? process.argv[replaceIndex + 1] ?? null : null;
14488
+ const projectFileId = replaceFileId ?? randomUUID5();
14489
+ const metadata = { projectFileId };
14490
+ if (preferredTemplateId !== null) {
14491
+ metadata.preferredTemplateId = preferredTemplateId;
14492
+ }
14493
+ if (replaceFileId !== null) {
14494
+ metadata.replaceFileId = replaceFileId;
14495
+ }
14496
+ const presign = await context.client.post(
14497
+ versionPath(context.workspace, "upload"),
14498
+ {
14499
+ route: "projectFile",
14500
+ metadata,
14501
+ files: [{ name, size: bytes.byteLength, type: mimeType }]
14502
+ }
14503
+ );
14504
+ const entries = Array.isArray(presign.files) ? presign.files : [];
14505
+ const entry = entries.filter(isObjectRecord2)[0];
14506
+ if (entry === void 0) {
14507
+ throw new Error("Upload presign returned no files.");
14508
+ }
14509
+ const signedUrl = entry.signedUrl;
14510
+ const fileInfo = isObjectRecord2(entry.file) ? entry.file : {};
14511
+ const objectInfo = isObjectRecord2(fileInfo.objectInfo) ? fileInfo.objectInfo : {};
14512
+ const storageKey = objectInfo.key;
14513
+ if (typeof signedUrl !== "string" || typeof storageKey !== "string") {
14514
+ throw new Error("Upload presign response is missing signedUrl/key.");
14515
+ }
14516
+ const putHeaders = { "Content-Type": mimeType };
14517
+ if (typeof objectInfo.cacheControl === "string") {
14518
+ putHeaders["Cache-Control"] = objectInfo.cacheControl;
14519
+ }
14520
+ if (isObjectRecord2(objectInfo.metadata)) {
14521
+ for (const [key, value] of Object.entries(objectInfo.metadata)) {
14522
+ if (typeof value === "string") putHeaders[`x-amz-meta-${key}`] = value;
14523
+ }
14524
+ }
14525
+ if (isObjectRecord2(entry.headers)) {
14526
+ for (const [key, value] of Object.entries(entry.headers)) {
14527
+ if (typeof value === "string") putHeaders[key] = value;
14528
+ }
14529
+ }
14530
+ const putResponse = await fetch(signedUrl, {
14531
+ method: "PUT",
14532
+ headers: putHeaders,
14533
+ body: new Uint8Array(bytes)
14534
+ });
14535
+ if (!putResponse.ok) {
14536
+ throw new Error(
14537
+ `Storage PUT failed (${putResponse.status}): ${await putResponse.text()}`
14538
+ );
14539
+ }
14540
+ const finalized = await context.client.post(
14541
+ versionPath(context.workspace, "files/finalize-upload"),
14542
+ {
14543
+ projectFileId,
14544
+ replaceFileId,
14545
+ name,
14546
+ mimeType,
14547
+ byteLength: bytes.byteLength,
14548
+ storageKey
14549
+ }
12645
14550
  );
14551
+ console.log(JSON.stringify(finalized, null, 2));
12646
14552
  }
12647
14553
  async function runFiles(context, subcommand, positional) {
14554
+ if (subcommand === "upload") {
14555
+ const filePath = positional[0];
14556
+ if (filePath === void 0) {
14557
+ throw new Error(
14558
+ "Usage: neo files upload <path> [--template <templateId>] [--replace <fileId>]"
14559
+ );
14560
+ }
14561
+ const templateIndex = process.argv.indexOf("--template");
14562
+ const preferredTemplateId = templateIndex >= 0 ? process.argv[templateIndex + 1] ?? null : null;
14563
+ await uploadProjectFile(context, filePath, preferredTemplateId);
14564
+ return;
14565
+ }
12648
14566
  if (subcommand === "list") {
12649
14567
  const { raw } = await fetchProjectDocument(context.workspace);
12650
14568
  const projectFiles = Array.isArray(raw.projectFiles) ? raw.projectFiles.filter(isObjectRecord2) : [];
@@ -12671,8 +14589,25 @@ async function runFiles(context, subcommand, positional) {
12671
14589
  console.log(JSON.stringify(result, null, 2));
12672
14590
  return;
12673
14591
  }
14592
+ if (subcommand === "texture-settings") {
14593
+ const fileId = positional[0];
14594
+ const fileFlagIndex = process.argv.indexOf("--file");
14595
+ const payloadPath = fileFlagIndex >= 0 ? process.argv[fileFlagIndex + 1] ?? null : null;
14596
+ if (fileId === void 0 || payloadPath === null) {
14597
+ throw new Error(
14598
+ "Usage: neo files texture-settings <fileId> --file <settings.json>"
14599
+ );
14600
+ }
14601
+ const payload = JSON.parse(readFileSync10(payloadPath, "utf8"));
14602
+ const result = await context.client.post(
14603
+ versionPath(context.workspace, `files/${fileId}/unity-texture-settings`),
14604
+ payload
14605
+ );
14606
+ console.log(JSON.stringify(result, null, 2));
14607
+ return;
14608
+ }
12674
14609
  throw new Error(
12675
- `Unknown files subcommand "${subcommand}" (list | delete; upload remains a web/Unity flow).`
14610
+ `Unknown files subcommand "${subcommand}" (upload | list | delete | texture-settings).`
12676
14611
  );
12677
14612
  }
12678
14613
  var init_content = __esm({
@@ -12686,13 +14621,64 @@ var init_content = __esm({
12686
14621
  }
12687
14622
  });
12688
14623
 
14624
+ // src/commands/export.ts
14625
+ var export_exports = {};
14626
+ __export(export_exports, {
14627
+ runExportUnity: () => runExportUnity
14628
+ });
14629
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
14630
+ import { join as join10 } from "node:path";
14631
+ async function runExportUnity(workspace, outDir) {
14632
+ if (outDir === null) {
14633
+ throw new Error(
14634
+ "Usage: neo export unity --out <UnityAssetsDir> (the Unity project's Assets folder)"
14635
+ );
14636
+ }
14637
+ const client = NeoApiClient.forBaseUrl(workspace.config.apiBaseUrl);
14638
+ const response = await client.post(
14639
+ `/api/projects/${workspace.config.projectId}/export`,
14640
+ { versionId: workspace.config.versionId }
14641
+ );
14642
+ const resourcesDir = join10(outDir, "Resources", "Neo");
14643
+ const localizationDir = join10(resourcesDir, "Localization");
14644
+ const scriptsDir = join10(outDir, "Scripts", "Neo");
14645
+ mkdirSync8(localizationDir, { recursive: true });
14646
+ mkdirSync8(scriptsDir, { recursive: true });
14647
+ writeFileSync8(join10(resourcesDir, "project.json"), response.projectJson);
14648
+ writeFileSync8(
14649
+ join10(scriptsDir, "NeoGeneratedTypes.cs"),
14650
+ response.generatedTypes
14651
+ );
14652
+ for (const file of response.localizationFiles ?? []) {
14653
+ writeFileSync8(join10(localizationDir, file.fileName), file.content);
14654
+ }
14655
+ console.log(`wrote ${join10(resourcesDir, "project.json")}`);
14656
+ console.log(`wrote ${join10(scriptsDir, "NeoGeneratedTypes.cs")}`);
14657
+ for (const file of response.localizationFiles ?? []) {
14658
+ console.log(`wrote ${join10(localizationDir, file.fileName)}`);
14659
+ }
14660
+ const diagnostics = response.diagnostics ?? [];
14661
+ for (const diagnostic of diagnostics) {
14662
+ console.log(`${diagnostic.severity}: ${diagnostic.message}`);
14663
+ }
14664
+ if (diagnostics.length === 0) {
14665
+ console.log("no export diagnostics");
14666
+ }
14667
+ }
14668
+ var init_export = __esm({
14669
+ "src/commands/export.ts"() {
14670
+ "use strict";
14671
+ init_http();
14672
+ }
14673
+ });
14674
+
12689
14675
  // src/commands/dev.ts
12690
14676
  var dev_exports = {};
12691
14677
  __export(dev_exports, {
12692
14678
  runDev: () => runDev
12693
14679
  });
12694
14680
  import { watch } from "node:fs";
12695
- import { join as join10 } from "node:path";
14681
+ import { join as join11 } from "node:path";
12696
14682
  import { emitKeypressEvents } from "node:readline";
12697
14683
  import { ConvexClient } from "convex/browser";
12698
14684
  function isSchemaSignal(value) {
@@ -12795,7 +14781,7 @@ async function runDev(workspace, options) {
12795
14781
  };
12796
14782
  for (const dir of ["Attributes", "Enums"]) {
12797
14783
  try {
12798
- watch(join10(workspace.root, dir), { persistent: true }, onFileChange);
14784
+ watch(join11(workspace.root, dir), { persistent: true }, onFileChange);
12799
14785
  } catch {
12800
14786
  }
12801
14787
  }
@@ -12880,13 +14866,19 @@ var EDITOR_SCOPES = [
12880
14866
  "project:localization:values:write",
12881
14867
  "project:localization:export",
12882
14868
  "project:localization:import",
12883
- "project:release-channel:read"
14869
+ "project:release-channel:read",
14870
+ // `neo export unity` writes project.json + NeoGeneratedTypes.cs headlessly
14871
+ // (the escape hatch when game code references not-yet-generated members and
14872
+ // a broken compile blocks the in-editor sync).
14873
+ "unity:export",
14874
+ // Branch lifecycle (auto-archive after `neo merge`, branch archive/restore)
14875
+ // is editor work; releases stay gated behind the release profile.
14876
+ "project:version:archive",
14877
+ "project:version:restore"
12884
14878
  ];
12885
14879
  var RELEASE_SCOPES = [
12886
14880
  ...EDITOR_SCOPES,
12887
14881
  "project:version:status:write",
12888
- "project:version:archive",
12889
- "project:version:restore",
12890
14882
  "project:release-channel:write",
12891
14883
  "project:release-channel:publish"
12892
14884
  ];
@@ -13103,6 +15095,12 @@ namespace NeoCompose.Schema
13103
15095
  public string? Name { get; set; }
13104
15096
  public string? ExtendsId { get; set; }
13105
15097
  public bool Locked { get; set; }
15098
+ /// <summary>
15099
+ /// Storage class override: "static", "save", or "session". Absent
15100
+ /// means the attribute inherits its placement parent's storage
15101
+ /// (specs/attribute-storage.md).
15102
+ /// </summary>
15103
+ public string? Storage { get; set; }
13106
15104
  public string? DefaultJson { get; set; }
13107
15105
  public string? ExtraJson { get; set; }
13108
15106
  protected NeoSchemaMemberAttribute(string? id) { Id = id; }
@@ -13115,6 +15113,11 @@ namespace NeoCompose.Schema
13115
15113
  public string? Name { get; set; }
13116
15114
  public string? ExtendsTypeId { get; set; }
13117
15115
  public bool Hidden { get; set; }
15116
+ /// <summary>
15117
+ /// Placement constraint: "static", "save", or "session". Absent
15118
+ /// means the type may be placed at any storage class.
15119
+ /// </summary>
15120
+ public string? AllowedStorage { get; set; }
13118
15121
  public string? ExtraJson { get; set; }
13119
15122
  public NeoCustomTypeAttribute(string? id = null) { Id = id; }
13120
15123
  public string? Id { get; }
@@ -13172,6 +15175,30 @@ namespace NeoCompose.Schema
13172
15175
  public NeoStringAttribute(string? id = null) : base(id) { }
13173
15176
  }
13174
15177
 
15178
+ [AttributeUsage(AttributeTargets.Property)]
15179
+ public sealed class NeoVector2Attribute : NeoSchemaMemberAttribute
15180
+ {
15181
+ public NeoVector2Attribute(string? id = null) : base(id) { }
15182
+ }
15183
+
15184
+ [AttributeUsage(AttributeTargets.Property)]
15185
+ public sealed class NeoVector2IntAttribute : NeoSchemaMemberAttribute
15186
+ {
15187
+ public NeoVector2IntAttribute(string? id = null) : base(id) { }
15188
+ }
15189
+
15190
+ [AttributeUsage(AttributeTargets.Property)]
15191
+ public sealed class NeoVector3Attribute : NeoSchemaMemberAttribute
15192
+ {
15193
+ public NeoVector3Attribute(string? id = null) : base(id) { }
15194
+ }
15195
+
15196
+ [AttributeUsage(AttributeTargets.Property)]
15197
+ public sealed class NeoVector3IntAttribute : NeoSchemaMemberAttribute
15198
+ {
15199
+ public NeoVector3IntAttribute(string? id = null) : base(id) { }
15200
+ }
15201
+
13175
15202
  [AttributeUsage(AttributeTargets.Property)]
13176
15203
  public sealed class NeoDictionaryAttribute : NeoSchemaMemberAttribute
13177
15204
  {
@@ -13266,6 +15293,10 @@ namespace NeoCompose.Schema
13266
15293
  public sealed class NeoAudioValue { }
13267
15294
  public sealed class NeoFunctionValue { }
13268
15295
  public sealed class NeoLookupRef { }
15296
+ public sealed class NeoVector2 { }
15297
+ public sealed class NeoVector2Int { }
15298
+ public sealed class NeoVector3 { }
15299
+ public sealed class NeoVector3Int { }
13269
15300
  }
13270
15301
  `;
13271
15302
  function scaffoldWorkspace(root) {
@@ -13502,7 +15533,7 @@ ${h("Branches & releases")}
13502
15533
  channel ${d("list | create | edit | delete")}
13503
15534
 
13504
15535
  ${h("Content & scripts")}
13505
- values ${d("list | get | set")} loc ${d("locales | list | set | status-set | comment-set")}
15536
+ values ${d("list | get | set")} loc ${d("locales | list | set | archive | restore | ...")}
13506
15537
  dialogue ${d("list | show | node | group | priority-group")}
13507
15538
  records ${d("get | query")} files ${d("list | delete")}
13508
15539
  script ${d("check | eval | apply")} migrate ${d("new | list | check | run | prune")}
@@ -13681,12 +15712,22 @@ async function main() {
13681
15712
  }
13682
15713
  return;
13683
15714
  }
15715
+ case "export": {
15716
+ const workspace = loadWorkspace(process.cwd());
15717
+ const sub = args.positional[0];
15718
+ if (sub !== "unity") {
15719
+ throw new Error("neo export requires a subcommand: unity.");
15720
+ }
15721
+ const { runExportUnity: runExportUnity2 } = await Promise.resolve().then(() => (init_export(), export_exports));
15722
+ await runExportUnity2(workspace, stringFlag(args, "out"));
15723
+ return;
15724
+ }
13684
15725
  case "script": {
13685
15726
  const workspace = loadWorkspace(process.cwd());
13686
15727
  const sub = args.positional[0];
13687
- if (sub !== "check" && sub !== "eval" && sub !== "apply") {
15728
+ if (sub !== "check" && sub !== "eval" && sub !== "apply" && sub !== "compile") {
13688
15729
  throw new Error(
13689
- "neo script requires a subcommand: check | eval | apply."
15730
+ "neo script requires a subcommand: check | eval | apply | compile."
13690
15731
  );
13691
15732
  }
13692
15733
  const { runScript: runScript2 } = await Promise.resolve().then(() => (init_script(), script_exports));