unity-agentic-tools 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2299,7 +2299,7 @@ var require_package = __commonJS((exports2, module2) => {
2299
2299
  module2.exports = {
2300
2300
  name: "unity-agentic-tools",
2301
2301
  packageManager: "bun@latest",
2302
- version: "0.4.1",
2302
+ version: "0.4.3",
2303
2303
  description: "Fast, token-efficient Unity YAML parser for AI agents",
2304
2304
  exports: {
2305
2305
  ".": {
@@ -2595,8 +2595,8 @@ function removeDirectoryRecursive(dir) {
2595
2595
  }
2596
2596
 
2597
2597
  // src/cmd-create.ts
2598
- var import_fs17 = require("fs");
2599
- var import_path7 = require("path");
2598
+ var import_fs18 = require("fs");
2599
+ var import_path10 = require("path");
2600
2600
  var import_crypto2 = require("crypto");
2601
2601
 
2602
2602
  // src/utils.ts
@@ -2619,6 +2619,9 @@ function find_unity_project_root(startDir) {
2619
2619
  }
2620
2620
  return null;
2621
2621
  }
2622
+ function resolve_project_path(project_path) {
2623
+ return import_path4.resolve(project_path || process.cwd());
2624
+ }
2622
2625
  function ensure_parent_dir(file_path) {
2623
2626
  const dir = import_path4.dirname(file_path);
2624
2627
  if (dir && dir !== "." && !import_fs4.existsSync(dir)) {
@@ -2806,6 +2809,19 @@ function build_guid_cache(project_path, raw) {
2806
2809
  }
2807
2810
  }
2808
2811
  return substringMatch;
2812
+ },
2813
+ find_all_by_name_exact(name, extension) {
2814
+ const nameLower = name.toLowerCase().replace(/\.[^.]+$/, "");
2815
+ const matches = [];
2816
+ for (const [guid, assetPath] of Object.entries(raw)) {
2817
+ if (extension && !assetPath.endsWith(extension))
2818
+ continue;
2819
+ const fileName = import_path5.basename(assetPath, import_path5.extname(assetPath)).toLowerCase();
2820
+ if (fileName === nameLower) {
2821
+ matches.push({ guid, path: assetPath });
2822
+ }
2823
+ }
2824
+ return matches;
2809
2825
  }
2810
2826
  };
2811
2827
  }
@@ -2904,15 +2920,14 @@ var UNITY_CLASS_IDS = {
2904
2920
  114: "MonoBehaviour",
2905
2921
  115: "MonoScript",
2906
2922
  117: "Texture3D",
2907
- 119: "NewAnimationTrack",
2908
- 120: "Projector",
2909
- 121: "LineRenderer",
2910
- 122: "Flare",
2911
- 123: "Halo",
2912
- 124: "LensFlare",
2913
- 125: "FlareLayer",
2914
- 126: "HaloLayer",
2915
- 127: "NavMeshProjectSettings",
2923
+ 118: "NewAnimationTrack",
2924
+ 119: "Projector",
2925
+ 120: "LineRenderer",
2926
+ 121: "Flare",
2927
+ 122: "Halo",
2928
+ 123: "LensFlare",
2929
+ 124: "FlareLayer",
2930
+ 126: "NavMeshProjectSettings",
2916
2931
  128: "Font",
2917
2932
  129: "PlayerSettings",
2918
2933
  130: "NamedObject",
@@ -2939,6 +2954,7 @@ var UNITY_CLASS_IDS = {
2939
2954
  164: "AudioReverbFilter",
2940
2955
  165: "AudioHighPassFilter",
2941
2956
  166: "AudioChorusFilter",
2957
+ 1660057539: "SceneRoots",
2942
2958
  167: "AudioReverbZone",
2943
2959
  168: "AudioEchoFilter",
2944
2960
  169: "AudioLowPassFilter",
@@ -3009,29 +3025,41 @@ var UNITY_CLASS_IDS = {
3009
3025
  257: "TargetJoint2D",
3010
3026
  258: "LightProbes",
3011
3027
  259: "LightProbeProxyVolume",
3012
- 260: "SampleClip",
3013
- 261: "AudioMixerSnapshot",
3014
- 262: "AudioMixerGroup",
3028
+ 260: "SampleClipLegacy",
3029
+ 261: "AudioMixerSnapshotLegacy",
3030
+ 262: "AudioMixerGroupLegacy",
3015
3031
  265: "NScreenBridge",
3016
- 271: "AssetBundleManifest",
3032
+ 271: "SampleClip",
3017
3033
  272: "UnityAdsManager",
3018
- 273: "RuntimeInitializeOnLoadManager",
3019
- 280: "UnityConnectSettings",
3020
- 281: "AvatarMask",
3021
- 290: "PlayableDirector",
3022
- 292: "VideoPlayer",
3023
- 293: "VideoClip",
3024
- 294: "ParticleSystemForceField",
3025
- 298: "SpriteMask",
3034
+ 273: "AudioMixerGroup",
3035
+ 280: "UnityConnectSettingsLegacy",
3036
+ 281: "AvatarMaskLegacy",
3037
+ 290: "AssetBundleManifest",
3038
+ 292: "VideoPlayerLegacy",
3039
+ 293: "VideoClipLegacy",
3040
+ 294: "ParticleSystemForceFieldLegacy",
3041
+ 298: "SpriteMaskLegacy",
3026
3042
  300: "WorldAnchor",
3027
- 301: "OcclusionCullingData",
3028
- 310: "PrefabInstance",
3029
- 319: "TextureImporter",
3030
- 363: "Preset",
3043
+ 301: "OcclusionCullingDataLegacy",
3044
+ 310: "UnityConnectSettings",
3045
+ 1001: "PrefabInstance",
3046
+ 319: "AvatarMask",
3047
+ 320: "PlayableDirector",
3048
+ 328: "VideoPlayer",
3049
+ 329: "VideoClip",
3050
+ 330: "ParticleSystemForceField",
3051
+ 331: "SpriteMask",
3052
+ 363: "OcclusionCullingData",
3053
+ 1006: "TextureImporter",
3054
+ 181963792: "Preset",
3031
3055
  687078895: "SpriteAtlas",
3056
+ 156049354: "Grid",
3057
+ 1742807556: "GridLayout",
3032
3058
  1839735485: "Tilemap",
3033
- 1839735486: "TilemapCollider2D",
3034
- 1839735487: "TilemapRenderer"
3059
+ 19719996: "TilemapCollider2D",
3060
+ 483693784: "TilemapRenderer",
3061
+ 1839735486: "TilemapCollider2DLegacy",
3062
+ 1839735487: "TilemapRendererLegacy"
3035
3063
  };
3036
3064
  var UNITY_CLASS_NAMES = Object.fromEntries(Object.entries(UNITY_CLASS_IDS).map(([id, name]) => [name, parseInt(id, 10)]));
3037
3065
  function get_class_id_name(class_id) {
@@ -3097,8 +3125,12 @@ function resolve_source_prefab(doc, file_path, project_path) {
3097
3125
  prefab_instance_block: pi_block
3098
3126
  };
3099
3127
  }
3100
- function resolveScriptGuid(script, projectPath) {
3128
+ function resolveScriptGuid(script, projectPath, options) {
3129
+ const strictExactName = options?.strict_exact_name === true;
3101
3130
  if (/^[a-f0-9]{32}$/i.test(script)) {
3131
+ if (/^0{32}$/i.test(script)) {
3132
+ throw new Error(`Invalid script GUID "${script}": all-zero GUID is not allowed. Provide a real script GUID from a .meta file.`);
3133
+ }
3102
3134
  return { guid: script.toLowerCase(), path: null };
3103
3135
  }
3104
3136
  if (script.endsWith(".cs")) {
@@ -3123,9 +3155,20 @@ function resolveScriptGuid(script, projectPath) {
3123
3155
  if (projectPath) {
3124
3156
  const guidCache = load_guid_cache(projectPath);
3125
3157
  if (guidCache) {
3126
- const result = guidCache.find_by_name(script, ".cs");
3127
- if (result)
3128
- return result;
3158
+ if (strictExactName) {
3159
+ const exactMatches = guidCache.find_all_by_name_exact(script, ".cs");
3160
+ if (exactMatches.length === 1) {
3161
+ return exactMatches[0];
3162
+ }
3163
+ if (exactMatches.length > 1) {
3164
+ const paths = exactMatches.map((m) => m.path).join(", ");
3165
+ throw new Error(`Ambiguous type "${script}": found ${exactMatches.length} exact script name matches (${paths}). ` + 'Use a qualified name (e.g., "Namespace.TypeName") or provide the full script path.');
3166
+ }
3167
+ } else {
3168
+ const result = guidCache.find_by_name(script, ".cs");
3169
+ if (result)
3170
+ return result;
3171
+ }
3129
3172
  }
3130
3173
  const registryPath = path.join(projectPath, ".unity-agentic", "type-registry.json");
3131
3174
  if (import_fs6.existsSync(registryPath)) {
@@ -3276,16 +3319,49 @@ function resolveScriptGuid(script, projectPath) {
3276
3319
  continue;
3277
3320
  try {
3278
3321
  const cache = JSON.parse(import_fs6.readFileSync(cachePath, "utf-8"));
3322
+ const exactMatches = [];
3279
3323
  for (const [guid, assetPath] of Object.entries(cache)) {
3280
3324
  if (!assetPath.endsWith(".cs"))
3281
3325
  continue;
3282
3326
  const fileName = path.basename(assetPath, ".cs").toLowerCase();
3283
3327
  if (fileName === scriptNameLower || classNameOnly && fileName === classNameOnly) {
3284
- return { guid, path: assetPath };
3328
+ exactMatches.push({ guid, path: assetPath });
3285
3329
  }
3286
3330
  }
3287
- } catch {}
3331
+ if (exactMatches.length === 1) {
3332
+ return exactMatches[0];
3333
+ }
3334
+ if (exactMatches.length > 1) {
3335
+ const paths = exactMatches.map((m) => m.path).join(", ");
3336
+ throw new Error(`Ambiguous type "${script}": found ${exactMatches.length} exact script name matches (${paths}). ` + 'Use a qualified name (e.g., "Namespace.TypeName") or provide the full script path.');
3337
+ }
3338
+ } catch (err) {
3339
+ if (err instanceof Error && err.message.startsWith("Ambiguous type")) {
3340
+ throw err;
3341
+ }
3342
+ }
3288
3343
  }
3344
+ if (strictExactName) {
3345
+ return null;
3346
+ }
3347
+ try {
3348
+ const assetsDir = path.join(projectPath, "Assets");
3349
+ if (import_fs6.existsSync(assetsDir)) {
3350
+ const entries = import_fs6.readdirSync(assetsDir, { recursive: true, withFileTypes: false });
3351
+ for (const entry of entries) {
3352
+ if (!entry.endsWith(".cs"))
3353
+ continue;
3354
+ const fileName = path.basename(entry, ".cs").toLowerCase();
3355
+ if (fileName === scriptNameLower || classNameOnly && fileName === classNameOnly) {
3356
+ const fullPath = path.join(assetsDir, entry);
3357
+ const guid = extractGuidFromMeta(fullPath + ".meta");
3358
+ if (guid) {
3359
+ return { guid, path: path.join("Assets", entry) };
3360
+ }
3361
+ }
3362
+ }
3363
+ }
3364
+ } catch {}
3289
3365
  }
3290
3366
  return null;
3291
3367
  }
@@ -3337,19 +3413,38 @@ function resolveAssetPathToPPtr(value, file_path, project_path) {
3337
3413
  return null;
3338
3414
  return `{fileID: ${mapping.fileID}, guid: ${guid}, type: ${mapping.type}}`;
3339
3415
  }
3340
- function resolve_script_with_fields(script, project_path) {
3341
- const resolved = resolveScriptGuid(script, project_path);
3416
+ function detect_abstract_class_in_source(file_path, class_name) {
3417
+ try {
3418
+ const source = import_fs6.readFileSync(file_path, "utf-8");
3419
+ const escaped_name = class_name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3420
+ const pattern = new RegExp(`(^|[^\\w])abstract\\s+class\\s+${escaped_name}(?=$|[^\\w])`, "m");
3421
+ return pattern.test(source);
3422
+ } catch {
3423
+ return false;
3424
+ }
3425
+ }
3426
+ function resolve_script_with_fields(script, project_path, options) {
3427
+ const resolved = resolveScriptGuid(script, project_path, options);
3342
3428
  if (!resolved)
3343
3429
  return null;
3344
3430
  const result = {
3345
3431
  guid: resolved.guid,
3346
3432
  path: resolved.path
3347
3433
  };
3434
+ if (resolved.path && resolved.path.endsWith(".cs")) {
3435
+ const full_path = path.isAbsolute(resolved.path) ? resolved.path : project_path ? path.join(project_path, resolved.path) : resolved.path;
3436
+ if (import_fs6.existsSync(full_path)) {
3437
+ const class_name = script.includes(".") ? script.split(".").pop() : script;
3438
+ if (class_name.length > 0) {
3439
+ result.is_abstract = detect_abstract_class_in_source(full_path, class_name);
3440
+ }
3441
+ }
3442
+ }
3348
3443
  if (!project_path)
3349
3444
  return result;
3350
3445
  if (resolved.path) {
3351
3446
  try {
3352
- const full_path = resolved.path.startsWith("/") ? resolved.path : path.join(project_path, resolved.path);
3447
+ const full_path = path.isAbsolute(resolved.path) ? resolved.path : path.join(project_path, resolved.path);
3353
3448
  if (!import_fs6.existsSync(full_path)) {
3354
3449
  result.extraction_error = `Script file not found at resolved path: ${full_path}`;
3355
3450
  } else if (resolved.path.endsWith(".cs")) {
@@ -3363,6 +3458,10 @@ function resolve_script_with_fields(script, project_path) {
3363
3458
  result.fields = chosen.fields;
3364
3459
  result.base_class = chosen.baseClass ?? undefined;
3365
3460
  result.kind = chosen.kind;
3461
+ result.class_name = chosen.name;
3462
+ if (chosen.name) {
3463
+ result.is_abstract = detect_abstract_class_in_source(full_path, chosen.name);
3464
+ }
3366
3465
  }
3367
3466
  } else {
3368
3467
  result.extraction_error = "Native extractSerializedFields function not available";
@@ -3440,8 +3539,8 @@ function build_type_lookup(project_path) {
3440
3539
  const assetsDir = path.join(project_path, "Assets");
3441
3540
  const csIndex = new Map;
3442
3541
  try {
3443
- const { readdirSync: readdirSync3 } = require("fs");
3444
- const entries = readdirSync3(assetsDir, { recursive: true, withFileTypes: false });
3542
+ const { readdirSync: readdirSync4 } = require("fs");
3543
+ const entries = readdirSync4(assetsDir, { recursive: true, withFileTypes: false });
3445
3544
  for (const entry of entries) {
3446
3545
  if (typeof entry === "string" && entry.endsWith(".cs")) {
3447
3546
  const basename3 = entry.substring(entry.lastIndexOf("/") + 1).replace(/\.cs$/, "");
@@ -4462,7 +4561,21 @@ class UnityDocument {
4462
4561
  }
4463
4562
  }
4464
4563
  }
4465
- return transform_ids;
4564
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4565
+ const name_pattern = new RegExp(`propertyPath: m_Name\\s*\\n\\s*value:\\s*${escaped}\\s*$`, "m");
4566
+ for (const block of this._blocks) {
4567
+ if ((block.class_id === 1001 || block.class_id === 310) && name_pattern.test(block.raw)) {
4568
+ const pi_id = block.file_id;
4569
+ const pi_ref_pattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${pi_id}\\}`);
4570
+ const stripped_transform = this._blocks.find((b) => (b.class_id === 4 || b.class_id === 224) && b.is_stripped && pi_ref_pattern.test(b.raw));
4571
+ if (stripped_transform) {
4572
+ transform_ids.push(stripped_transform.file_id);
4573
+ } else {
4574
+ transform_ids.push(pi_id);
4575
+ }
4576
+ }
4577
+ }
4578
+ return Array.from(new Set(transform_ids));
4466
4579
  }
4467
4580
  require_unique_game_object(name_or_id) {
4468
4581
  if (/^-?\d+$/.test(name_or_id)) {
@@ -4491,10 +4604,7 @@ class UnityDocument {
4491
4604
  if (!block) {
4492
4605
  return { error: `Block with fileID ${name_or_id} not found` };
4493
4606
  }
4494
- if (block.class_id === 4) {
4495
- return block;
4496
- }
4497
- if (block.class_id === 224) {
4607
+ if (block.class_id === 4 || block.class_id === 224 || block.class_id === 1001 || block.class_id === 310) {
4498
4608
  return block;
4499
4609
  }
4500
4610
  if (block.class_id === 1) {
@@ -4507,15 +4617,40 @@ class UnityDocument {
4507
4617
  }
4508
4618
  return { error: `Transform for GameObject fileID ${name_or_id} not found` };
4509
4619
  }
4510
- return { error: `fileID ${name_or_id} is not a GameObject or Transform (class ${block.class_id})` };
4620
+ return { error: `fileID ${name_or_id} is not a GameObject, Transform, or PrefabInstance (class ${block.class_id})` };
4511
4621
  }
4512
4622
  const transform_ids = this.find_transforms_by_name(name_or_id);
4513
4623
  if (transform_ids.length === 0) {
4514
- return { error: `GameObject "${name_or_id}" not found` };
4624
+ return { error: `Object "${name_or_id}" not found` };
4515
4625
  }
4516
4626
  if (transform_ids.length > 1) {
4517
- const go_ids = this.find_game_objects_by_name(name_or_id).map((b) => b.file_id).join(", ");
4518
- return { error: `Multiple GameObjects named "${name_or_id}" found (fileIDs: ${go_ids}). Use numeric fileID to specify which one.` };
4627
+ const entity_ids = [];
4628
+ for (const tid of transform_ids) {
4629
+ const block = this.find_by_file_id(tid);
4630
+ if (block) {
4631
+ if (block.class_id === 4 || block.class_id === 224) {
4632
+ const go_match = block.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
4633
+ if (go_match) {
4634
+ entity_ids.push(go_match[1]);
4635
+ } else {
4636
+ entity_ids.push(tid);
4637
+ }
4638
+ } else if (block.class_id === 1001 || block.class_id === 310) {
4639
+ entity_ids.push(tid);
4640
+ } else if (block.is_stripped) {
4641
+ const pi_match = block.raw.match(/m_PrefabInstance:\s*\{fileID:\s*(-?\d+)\}/);
4642
+ if (pi_match) {
4643
+ entity_ids.push(pi_match[1]);
4644
+ } else {
4645
+ entity_ids.push(tid);
4646
+ }
4647
+ } else {
4648
+ entity_ids.push(tid);
4649
+ }
4650
+ }
4651
+ }
4652
+ const ids_str = Array.from(new Set(entity_ids)).join(", ");
4653
+ return { error: `Multiple GameObjects/PrefabInstances named "${name_or_id}" found (fileIDs: ${ids_str}). Use numeric fileID to specify which one.` };
4519
4654
  }
4520
4655
  const transform = this.find_by_file_id(transform_ids[0]);
4521
4656
  if (!transform) {
@@ -4543,7 +4678,7 @@ class UnityDocument {
4543
4678
  if (t_block.class_id === 4 && t_block.is_stripped) {
4544
4679
  let name = "Variant";
4545
4680
  for (const pi_block of this._blocks) {
4546
- if (pi_block.class_id === 1001) {
4681
+ if (pi_block.class_id === 1001 || pi_block.class_id === 310) {
4547
4682
  const name_mod = pi_block.raw.match(/propertyPath: m_Name\s*\n\s*value:\s*(.+)/);
4548
4683
  if (name_mod) {
4549
4684
  name = name_mod[1].trim();
@@ -4696,7 +4831,7 @@ class UnityDocument {
4696
4831
  }
4697
4832
  add_child_to_parent(parent_id, child_id) {
4698
4833
  const parent = this.find_by_file_id(parent_id);
4699
- if (!parent || parent.class_id !== 4)
4834
+ if (!parent || parent.class_id !== 4 && parent.class_id !== 224)
4700
4835
  return false;
4701
4836
  let raw = parent.raw;
4702
4837
  const inline_empty = /m_Children:\s*\[\]/;
@@ -4732,7 +4867,7 @@ class UnityDocument {
4732
4867
  }
4733
4868
  remove_child_from_parent(parent_id, child_id) {
4734
4869
  const parent = this.find_by_file_id(parent_id);
4735
- if (!parent || parent.class_id !== 4)
4870
+ if (!parent || parent.class_id !== 4 && parent.class_id !== 224)
4736
4871
  return false;
4737
4872
  let raw = parent.raw;
4738
4873
  const child_line = new RegExp(`\\n[ \\t]*- \\{fileID: ${child_id}\\}`);
@@ -4753,7 +4888,49 @@ class UnityDocument {
4753
4888
  }
4754
4889
  _collect_hierarchy_recursive(transform_id, result) {
4755
4890
  const transform = this.find_by_file_id(transform_id);
4756
- if (!transform || transform.class_id !== 4)
4891
+ if (!transform)
4892
+ return;
4893
+ if (transform.class_id === 1001 || transform.class_id === 310) {
4894
+ const pi_id = transform.file_id;
4895
+ const pi_ref_pattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${pi_id}\\}`);
4896
+ for (const block of this._blocks) {
4897
+ if (block.is_stripped && pi_ref_pattern.test(block.raw)) {
4898
+ result.add(block.file_id);
4899
+ if (block.class_id === 4 || block.class_id === 224) {
4900
+ this._collect_hierarchy_recursive(block.file_id, result);
4901
+ }
4902
+ }
4903
+ }
4904
+ const added_go_matches = transform.raw.matchAll(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)\}/g);
4905
+ for (const match of added_go_matches) {
4906
+ const added_id = match[1];
4907
+ if (added_id !== "0" && !result.has(added_id)) {
4908
+ result.add(added_id);
4909
+ const added_block = this.find_by_file_id(added_id);
4910
+ if (added_block && added_block.class_id === 1) {
4911
+ const comp_matches = added_block.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
4912
+ for (const cm of comp_matches) {
4913
+ const comp_block = this.find_by_file_id(cm[1]);
4914
+ if (comp_block && (comp_block.class_id === 4 || comp_block.class_id === 224)) {
4915
+ if (!result.has(cm[1])) {
4916
+ result.add(cm[1]);
4917
+ this._collect_hierarchy_recursive(cm[1], result);
4918
+ }
4919
+ break;
4920
+ }
4921
+ }
4922
+ }
4923
+ }
4924
+ }
4925
+ const added_comp_matches = transform.raw.matchAll(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)\}/g);
4926
+ for (const match of added_comp_matches) {
4927
+ const added_id = match[1];
4928
+ if (added_id !== "0")
4929
+ result.add(added_id);
4930
+ }
4931
+ return;
4932
+ }
4933
+ if (transform.class_id !== 4 && transform.class_id !== 224)
4757
4934
  return;
4758
4935
  const children_section = transform.raw.match(/m_Children:[\s\S]*?(?=\s*m_Father:)/);
4759
4936
  if (!children_section)
@@ -4788,14 +4965,21 @@ class UnityDocument {
4788
4965
  if (parent_id === "0") {
4789
4966
  let count = 0;
4790
4967
  for (const block of this._blocks) {
4791
- if (block.class_id === 4 && /m_Father:\s*\{fileID:\s*0\}/.test(block.raw)) {
4968
+ if ((block.class_id === 4 || block.class_id === 224) && /m_Father:\s*\{fileID:\s*0\}/.test(block.raw)) {
4792
4969
  count++;
4970
+ } else if ((block.class_id === 1001 || block.class_id === 310) && /m_TransformParent:\s*\{fileID:\s*0\}/.test(block.raw)) {
4971
+ const pi_id = block.file_id;
4972
+ const pi_ref_pattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${pi_id}\\}`);
4973
+ const has_stripped = this._blocks.some((b) => (b.class_id === 4 || b.class_id === 224) && b.is_stripped && pi_ref_pattern.test(b.raw));
4974
+ if (!has_stripped) {
4975
+ count++;
4976
+ }
4793
4977
  }
4794
4978
  }
4795
4979
  return count;
4796
4980
  }
4797
4981
  const parent = this.find_by_file_id(parent_id);
4798
- if (!parent || parent.class_id !== 4)
4982
+ if (!parent || parent.class_id !== 4 && parent.class_id !== 224)
4799
4983
  return 0;
4800
4984
  const children_match = parent.raw.match(/m_Children:[\s\S]*?(?=\s*m_Father:)/);
4801
4985
  if (children_match) {
@@ -4828,19 +5012,97 @@ ${new_children}`);
4828
5012
  }
4829
5013
  return false;
4830
5014
  }
5015
+ add_root_to_scene_roots(root_id) {
5016
+ const pi_block = this.find_by_file_id(root_id);
5017
+ if (pi_block && (pi_block.class_id === 1001 || pi_block.class_id === 310)) {
5018
+ const pi_ref_pattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${root_id}\\}`);
5019
+ const stripped_root = this._blocks.find((b) => (b.class_id === 4 || b.class_id === 224) && b.is_stripped && pi_ref_pattern.test(b.raw) && /m_Father:\s*\{fileID:\s*0\}/.test(b.raw));
5020
+ if (stripped_root) {
5021
+ root_id = stripped_root.file_id;
5022
+ }
5023
+ }
5024
+ const sr_blocks = [...this.find_by_class_id(166), ...this.find_by_class_id(1660057539)];
5025
+ if (sr_blocks.length === 0)
5026
+ return false;
5027
+ const sr_block = sr_blocks[0];
5028
+ let raw = sr_block.raw;
5029
+ const roots_pattern = /(m_Roots:\s*\n(?:\s*-\s*\{fileID:\s*-?\d+\}\s*\n)*)/;
5030
+ if (roots_pattern.test(raw)) {
5031
+ raw = raw.replace(roots_pattern, `$1 - {fileID: ${root_id}}
5032
+ `);
5033
+ sr_block.replace_raw(raw);
5034
+ return true;
5035
+ }
5036
+ const empty_roots = /m_Roots:\s*\[\]/;
5037
+ if (empty_roots.test(raw)) {
5038
+ raw = raw.replace(empty_roots, `m_Roots:
5039
+ - {fileID: ${root_id}}`);
5040
+ sr_block.replace_raw(raw);
5041
+ return true;
5042
+ }
5043
+ return false;
5044
+ }
5045
+ remove_root_from_scene_roots(root_id) {
5046
+ const pi_block = this.find_by_file_id(root_id);
5047
+ if (pi_block && (pi_block.class_id === 1001 || pi_block.class_id === 310)) {
5048
+ const pi_ref_pattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${root_id}\\}`);
5049
+ const stripped_root = this._blocks.find((b) => (b.class_id === 4 || b.class_id === 224) && b.is_stripped && pi_ref_pattern.test(b.raw));
5050
+ if (stripped_root) {
5051
+ this._remove_id_from_scene_roots(stripped_root.file_id);
5052
+ }
5053
+ }
5054
+ return this._remove_id_from_scene_roots(root_id);
5055
+ }
5056
+ _remove_id_from_scene_roots(id) {
5057
+ const sr_blocks = [...this.find_by_class_id(166), ...this.find_by_class_id(1660057539)];
5058
+ if (sr_blocks.length === 0)
5059
+ return false;
5060
+ const sr_block = sr_blocks[0];
5061
+ let raw = sr_block.raw;
5062
+ const root_line = new RegExp(`\\n[ \\t]*- \\{fileID: ${id}\\}`);
5063
+ if (!root_line.test(raw))
5064
+ return false;
5065
+ raw = raw.replace(root_line, "");
5066
+ sr_block.replace_raw(raw);
5067
+ return true;
5068
+ }
4831
5069
  reorder_entities(ordered_transform_ids) {
4832
5070
  const entity_groups = [];
4833
5071
  for (const transform_id of ordered_transform_ids) {
4834
- const transform = this.find_by_file_id(transform_id);
4835
- if (!transform)
5072
+ const block = this.find_by_file_id(transform_id);
5073
+ if (!block)
5074
+ continue;
5075
+ let pi_block = null;
5076
+ if (block.class_id === 1001 || block.class_id === 310) {
5077
+ pi_block = block;
5078
+ } else if (block.is_stripped) {
5079
+ const pi_match = block.raw.match(/m_PrefabInstance:\s*\{fileID:\s*(-?\d+)\}/);
5080
+ if (pi_match) {
5081
+ const found = this.find_by_file_id(pi_match[1]);
5082
+ if (found && (found.class_id === 1001 || found.class_id === 310)) {
5083
+ pi_block = found;
5084
+ }
5085
+ }
5086
+ }
5087
+ if (pi_block) {
5088
+ const group = [pi_block.file_id];
5089
+ const hierarchy = this.collect_hierarchy(pi_block.file_id);
5090
+ for (const id of hierarchy)
5091
+ group.push(id);
5092
+ entity_groups.push(group);
4836
5093
  continue;
4837
- const go_match = transform.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
4838
- if (!go_match)
5094
+ }
5095
+ const go_match = block.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
5096
+ if (!go_match) {
5097
+ entity_groups.push([transform_id]);
4839
5098
  continue;
5099
+ }
4840
5100
  const go_id = go_match[1];
4841
5101
  const go = this.find_by_file_id(go_id);
4842
- if (!go)
5102
+ if (!go) {
5103
+ entity_groups.push([transform_id]);
4843
5104
  continue;
5105
+ }
4844
5106
  const comp_ids = [];
4845
5107
  const comp_matches = go.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
4846
5108
  for (const m of comp_matches) {
@@ -5223,48 +5485,320 @@ function find_game_object_in_variant(doc, name, file_path, project_path) {
5223
5485
  }
5224
5486
  return null;
5225
5487
  }
5226
- function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
5227
- const piBlock = doc.find_by_file_id(piId);
5228
- if (!piBlock)
5229
- return;
5230
- const entry = `
5231
- - targetCorrespondingSourceObject: ${targetSourceRef}
5232
- insertIndex: -1
5233
- addedObject: {fileID: ${newGoId}}`;
5234
- let raw = piBlock.raw;
5235
- const emptyPattern = /m_AddedGameObjects:[ \t]*\[\]/;
5236
- if (emptyPattern.test(raw)) {
5237
- raw = raw.replace(emptyPattern, `m_AddedGameObjects:${entry}`);
5238
- piBlock.replace_raw(raw);
5239
- return;
5488
+ function parsePrefabRef(refText) {
5489
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
5490
+ if (!fileIdMatch)
5491
+ return null;
5492
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
5493
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
5494
+ return {
5495
+ file_id: fileIdMatch[1],
5496
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
5497
+ type: typeMatch ? typeMatch[1] : undefined
5498
+ };
5499
+ }
5500
+ function extractPrefabSourceGuid(prefabInstanceRaw) {
5501
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
5502
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
5503
+ }
5504
+ function normalizePrefabRef(refText, sourceGuid) {
5505
+ const parsed = parsePrefabRef(refText);
5506
+ if (!parsed) {
5507
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
5508
+ }
5509
+ const guid = parsed.guid ?? sourceGuid;
5510
+ if (!guid) {
5511
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
5240
5512
  }
5241
- const existingPattern = /(m_AddedGameObjects:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
5242
- if (existingPattern.test(raw)) {
5243
- raw = raw.replace(existingPattern, `$1${entry}`);
5244
- piBlock.replace_raw(raw);
5513
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
5514
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
5245
5515
  }
5516
+ const type = parsed.type ?? "3";
5517
+ if (!/^-?\d+$/.test(type)) {
5518
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
5519
+ }
5520
+ return {
5521
+ ref: {
5522
+ file_id: parsed.file_id,
5523
+ guid,
5524
+ type
5525
+ }
5526
+ };
5246
5527
  }
5247
- function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
5248
- const piBlock = doc.find_by_file_id(piId);
5249
- if (!piBlock)
5250
- return;
5251
- const entry = `
5252
- - targetCorrespondingSourceObject: ${targetSourceRef}
5253
- insertIndex: -1
5254
- addedObject: {fileID: ${newComponentId}}`;
5255
- let raw = piBlock.raw;
5256
- const emptyPattern = /m_AddedComponents:[ \t]*\[\]/;
5257
- if (emptyPattern.test(raw)) {
5258
- raw = raw.replace(emptyPattern, `m_AddedComponents:${entry}`);
5259
- piBlock.replace_raw(raw);
5260
- return;
5528
+ function serializePrefabRef(ref) {
5529
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
5530
+ }
5531
+ function ensurePrefabModificationSerializedVersion(prefabInstanceBlock) {
5532
+ const raw = prefabInstanceBlock.raw;
5533
+ const updated = raw.replace(/(^([ \t]*)m_Modification:[ \t]*\n)(?!\2[ \t]+serializedVersion:[ \t]*\d+)/m, (_match, prefix, indent) => `${prefix}${indent} serializedVersion: 3
5534
+ `);
5535
+ if (updated === raw) {
5536
+ return false;
5537
+ }
5538
+ prefabInstanceBlock.replace_raw(updated);
5539
+ return true;
5540
+ }
5541
+ function addedOverrideEntryKey(entry) {
5542
+ return `${entry.target.file_id}|${entry.target.guid}|${entry.target.type}|${entry.insert_index}|${entry.added_object_file_id}`;
5543
+ }
5544
+ function splitPrefabArraySection(rawText, key) {
5545
+ const lines = rawText.split(`
5546
+ `);
5547
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
5548
+ let keyIndex = -1;
5549
+ let indent = "";
5550
+ for (let i = 0;i < lines.length; i++) {
5551
+ const match = lines[i].match(keyPattern);
5552
+ if (match) {
5553
+ keyIndex = i;
5554
+ indent = match[1];
5555
+ break;
5556
+ }
5557
+ }
5558
+ if (keyIndex === -1) {
5559
+ return null;
5560
+ }
5561
+ const keyIndentLen = indent.length;
5562
+ let sectionEnd = keyIndex + 1;
5563
+ for (let i = keyIndex + 1;i < lines.length; i++) {
5564
+ const line = lines[i];
5565
+ const trimmed = line.trim();
5566
+ if (trimmed.length === 0) {
5567
+ sectionEnd = i + 1;
5568
+ continue;
5569
+ }
5570
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
5571
+ if (indentLen < keyIndentLen) {
5572
+ break;
5573
+ }
5574
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
5575
+ break;
5576
+ }
5577
+ sectionEnd = i + 1;
5578
+ }
5579
+ return {
5580
+ lines,
5581
+ key_index: keyIndex,
5582
+ section_end: sectionEnd,
5583
+ indent
5584
+ };
5585
+ }
5586
+ function collectAddedOverrideEntries(lines, key_index, section_end, sourceGuid) {
5587
+ const sectionLines = lines.slice(key_index + 1, section_end);
5588
+ const dedup = new Map;
5589
+ let i = 0;
5590
+ while (i < sectionLines.length) {
5591
+ const line = sectionLines[i];
5592
+ const trimmed = line.trimStart();
5593
+ if (!trimmed.startsWith("-")) {
5594
+ i++;
5595
+ continue;
5596
+ }
5597
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
5598
+ const entryLines = [line];
5599
+ i++;
5600
+ while (i < sectionLines.length) {
5601
+ const nextLine = sectionLines[i];
5602
+ const nextTrimmed = nextLine.trim();
5603
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
5604
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
5605
+ break;
5606
+ }
5607
+ entryLines.push(nextLine);
5608
+ i++;
5609
+ }
5610
+ const entryText = entryLines.join(`
5611
+ `);
5612
+ const targetMatch = entryText.match(/targetCorrespondingSourceObject:[ \t]*(\{[^}]+\})/);
5613
+ const insertMatch = entryText.match(/insertIndex:[ \t]*(-?\d+)/);
5614
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
5615
+ if (!targetMatch || !addedObjectMatch) {
5616
+ continue;
5617
+ }
5618
+ const normalizedTarget = normalizePrefabRef(targetMatch[1], sourceGuid).ref;
5619
+ if (!normalizedTarget) {
5620
+ continue;
5621
+ }
5622
+ const insertIndex = insertMatch ? Number.parseInt(insertMatch[1], 10) : -1;
5623
+ const entry = {
5624
+ target: normalizedTarget,
5625
+ insert_index: Number.isNaN(insertIndex) ? -1 : insertIndex,
5626
+ added_object_file_id: addedObjectMatch[1]
5627
+ };
5628
+ const dedupKey = addedOverrideEntryKey(entry);
5629
+ if (!dedup.has(dedupKey)) {
5630
+ dedup.set(dedupKey, entry);
5631
+ }
5261
5632
  }
5262
- const existingPattern = /(m_AddedComponents:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
5263
- if (existingPattern.test(raw)) {
5264
- raw = raw.replace(existingPattern, `$1${entry}`);
5265
- piBlock.replace_raw(raw);
5633
+ return [...dedup.values()];
5634
+ }
5635
+ function appendToAddedOverrideArray(doc, piId, key, targetSourceRef, newObjectId) {
5636
+ const piBlock = doc.find_by_file_id(piId);
5637
+ if (!piBlock) {
5638
+ return { success: false, error: `PrefabInstance with fileID ${piId} not found` };
5639
+ }
5640
+ ensurePrefabModificationSerializedVersion(piBlock);
5641
+ const split = splitPrefabArraySection(piBlock.raw, key);
5642
+ if (!split) {
5643
+ return { success: false, error: `${key} property not found in PrefabInstance ${piId}` };
5644
+ }
5645
+ const sourceGuid = extractPrefabSourceGuid(piBlock.raw);
5646
+ const normalizedTarget = normalizePrefabRef(targetSourceRef, sourceGuid);
5647
+ if (!normalizedTarget.ref) {
5648
+ return { success: false, error: normalizedTarget.error };
5649
+ }
5650
+ const candidate = {
5651
+ target: normalizedTarget.ref,
5652
+ insert_index: -1,
5653
+ added_object_file_id: newObjectId
5654
+ };
5655
+ const candidateKey = addedOverrideEntryKey(candidate);
5656
+ const existing = collectAddedOverrideEntries(split.lines, split.key_index, split.section_end, sourceGuid);
5657
+ if (existing.some((entry) => addedOverrideEntryKey(entry) === candidateKey)) {
5658
+ return { success: true, changed: false };
5659
+ }
5660
+ const entryLines = [
5661
+ `${split.indent}- targetCorrespondingSourceObject: ${serializePrefabRef(candidate.target)}`,
5662
+ `${split.indent} insertIndex: ${candidate.insert_index}`,
5663
+ `${split.indent} addedObject: {fileID: ${candidate.added_object_file_id}}`
5664
+ ];
5665
+ const keyLine = split.lines[split.key_index].trim();
5666
+ let insertIndex = split.section_end;
5667
+ while (insertIndex > split.key_index + 1 && split.lines[insertIndex - 1].trim().length === 0) {
5668
+ insertIndex--;
5669
+ }
5670
+ let updatedLines;
5671
+ if (keyLine === `${key}: []`) {
5672
+ const replacement = [`${split.indent}${key}:`, ...entryLines];
5673
+ updatedLines = [
5674
+ ...split.lines.slice(0, split.key_index),
5675
+ ...replacement,
5676
+ ...split.lines.slice(split.section_end)
5677
+ ];
5678
+ } else {
5679
+ updatedLines = [
5680
+ ...split.lines.slice(0, insertIndex),
5681
+ ...entryLines,
5682
+ ...split.lines.slice(insertIndex)
5683
+ ];
5266
5684
  }
5685
+ piBlock.replace_raw(updatedLines.join(`
5686
+ `));
5687
+ return { success: true, changed: true };
5688
+ }
5689
+ function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
5690
+ return appendToAddedOverrideArray(doc, piId, "m_AddedGameObjects", targetSourceRef, newGoId);
5691
+ }
5692
+ function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
5693
+ return appendToAddedOverrideArray(doc, piId, "m_AddedComponents", targetSourceRef, newComponentId);
5267
5694
  }
5695
+ var COMPONENT_FULL_TEMPLATES = {
5696
+ 65: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
5697
+ m_Material: {fileID: 0}
5698
+ m_IncludeLayers:
5699
+ serializedVersion: 2
5700
+ m_Bits: 0
5701
+ m_ExcludeLayers:
5702
+ serializedVersion: 2
5703
+ m_Bits: 0
5704
+ m_LayerOverridePriority: 0
5705
+ m_IsTrigger: 0
5706
+ m_ProvidesContacts: 0
5707
+ m_Enabled: 1
5708
+ serializedVersion: 3
5709
+ m_Size: {x: 1, y: 1, z: 1}
5710
+ m_Center: {x: 0, y: 0, z: 0}`,
5711
+ 82: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
5712
+ m_Enabled: 1
5713
+ serializedVersion: 4
5714
+ OutputAudioMixerGroup: {fileID: 0}
5715
+ m_audioClip: {fileID: 0}
5716
+ m_PlayOnAwake: 1
5717
+ m_Volume: 1
5718
+ m_Pitch: 1
5719
+ Loop: 0
5720
+ Mute: 0
5721
+ Spatialize: 0
5722
+ SpatializePostEffects: 0
5723
+ Priority: 128
5724
+ DopplerLevel: 1
5725
+ MinDistance: 1
5726
+ MaxDistance: 500
5727
+ Pan2D: 0
5728
+ rolloffMode: 0
5729
+ BypassEffects: 0
5730
+ BypassListenerEffects: 0
5731
+ BypassReverbZones: 0
5732
+ rolloffCustomCurve:
5733
+ serializedVersion: 2
5734
+ m_Curve:
5735
+ - serializedVersion: 3
5736
+ time: 0
5737
+ value: 1
5738
+ inSlope: 0
5739
+ outSlope: 0
5740
+ tangentMode: 0
5741
+ weightedMode: 0
5742
+ inWeight: 0.33333334
5743
+ outWeight: 0.33333334
5744
+ - serializedVersion: 3
5745
+ time: 1
5746
+ value: 0
5747
+ inSlope: 0
5748
+ outSlope: 0
5749
+ tangentMode: 0
5750
+ weightedMode: 0
5751
+ inWeight: 0.33333334
5752
+ outWeight: 0.33333334
5753
+ m_PreInfinity: 2
5754
+ m_PostInfinity: 2
5755
+ m_RotationOrder: 4
5756
+ panLevelCustomCurve:
5757
+ serializedVersion: 2
5758
+ m_Curve:
5759
+ - serializedVersion: 3
5760
+ time: 0
5761
+ value: 0
5762
+ inSlope: 0
5763
+ outSlope: 0
5764
+ tangentMode: 0
5765
+ weightedMode: 0
5766
+ inWeight: 0.33333334
5767
+ outWeight: 0.33333334
5768
+ m_PreInfinity: 2
5769
+ m_PostInfinity: 2
5770
+ m_RotationOrder: 4
5771
+ spreadCustomCurve:
5772
+ serializedVersion: 2
5773
+ m_Curve:
5774
+ - serializedVersion: 3
5775
+ time: 0
5776
+ value: 0
5777
+ inSlope: 0
5778
+ outSlope: 0
5779
+ tangentMode: 0
5780
+ weightedMode: 0
5781
+ inWeight: 0.33333334
5782
+ outWeight: 0.33333334
5783
+ m_PreInfinity: 2
5784
+ m_PostInfinity: 2
5785
+ m_RotationOrder: 4
5786
+ reverbZoneMixCustomCurve:
5787
+ serializedVersion: 2
5788
+ m_Curve:
5789
+ - serializedVersion: 3
5790
+ time: 0
5791
+ value: 1
5792
+ inSlope: 0
5793
+ outSlope: 0
5794
+ tangentMode: 0
5795
+ weightedMode: 0
5796
+ inWeight: 0.33333334
5797
+ outWeight: 0.33333334
5798
+ m_PreInfinity: 2
5799
+ m_PostInfinity: 2
5800
+ m_RotationOrder: 4`
5801
+ };
5268
5802
  var COMPONENT_DEFAULTS = {
5269
5803
  20: ` serializedVersion: 2
5270
5804
  m_ClearFlags: 1
@@ -5391,6 +5925,17 @@ var COMPONENT_DEFAULTS = {
5391
5925
  m_IgnoreParentGroups: 0`
5392
5926
  };
5393
5927
  function createGenericComponentYAML(componentName, classId, componentId, gameObjectId) {
5928
+ const fullTemplate = COMPONENT_FULL_TEMPLATES[classId];
5929
+ if (fullTemplate) {
5930
+ return `--- !u!${classId} &${componentId}
5931
+ ${componentName}:
5932
+ m_ObjectHideFlags: 0
5933
+ m_CorrespondingSourceObject: {fileID: 0}
5934
+ m_PrefabInstance: {fileID: 0}
5935
+ m_PrefabAsset: {fileID: 0}
5936
+ ${fullTemplate(gameObjectId)}
5937
+ `;
5938
+ }
5394
5939
  const defaults = COMPONENT_DEFAULTS[classId] ? `
5395
5940
  ` + COMPONENT_DEFAULTS[classId] + `
5396
5941
  ` : "";
@@ -5543,7 +6088,14 @@ function createGameObject(options) {
5543
6088
  doc.add_child_to_parent(parentTransformIdStr, transformIdStr);
5544
6089
  }
5545
6090
  if (variantPiId && strippedSourceRef) {
5546
- appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
6091
+ const appendResult = appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
6092
+ if (!appendResult.success) {
6093
+ return {
6094
+ success: false,
6095
+ file_path,
6096
+ error: appendResult.error || `Failed to update m_AddedGameObjects for PrefabInstance ${variantPiId}`
6097
+ };
6098
+ }
5547
6099
  }
5548
6100
  const saveResult = doc.save();
5549
6101
  if (!saveResult.success) {
@@ -6026,6 +6578,7 @@ PrefabInstance:
6026
6578
  m_ObjectHideFlags: 0
6027
6579
  serializedVersion: 2
6028
6580
  m_Modification:
6581
+ serializedVersion: 3
6029
6582
  m_TransformParent: {fileID: 0}
6030
6583
  m_Modifications:
6031
6584
  - target: {fileID: ${rootInfo.game_object.file_id}, guid: ${sourceGuid}, type: 3}
@@ -6172,6 +6725,7 @@ PrefabInstance:
6172
6725
  m_ObjectHideFlags: 0
6173
6726
  serializedVersion: 2
6174
6727
  m_Modification:
6728
+ serializedVersion: 3
6175
6729
  m_TransformParent: {fileID: ${parentTransformIdStr}}
6176
6730
  m_Modifications:
6177
6731
  - target: {fileID: ${rootGoFileId}, guid: ${sourceGuid}, type: 3}
@@ -6276,6 +6830,9 @@ function createScriptableObject(options) {
6276
6830
  if (resolved.kind === "enum" || resolved.kind === "interface") {
6277
6831
  return { success: false, output_path, error: `"${script}" is ${resolved.kind === "enum" ? "an enum" : "an interface"}, not a ScriptableObject.` };
6278
6832
  }
6833
+ if (resolved.is_abstract) {
6834
+ return { success: false, output_path, error: `"${script}" is abstract and cannot be instantiated as a ScriptableObject asset.` };
6835
+ }
6279
6836
  if (resolved.base_class && resolved.base_class !== "ScriptableObject") {
6280
6837
  return { success: false, output_path, error: `"${script}" extends ${resolved.base_class}, not ScriptableObject. Cannot create as a ScriptableObject asset.` };
6281
6838
  }
@@ -6476,6 +7033,8 @@ function is_valid_component_base(base_class, project_path, depth = 0) {
6476
7033
  }
6477
7034
  if (!fullPath.endsWith(".cs") || !import_fs8.existsSync(fullPath)) {
6478
7035
  if (match.kind === "class") {
7036
+ if (fullPath.includes("Assembly-CSharp.dll"))
7037
+ return true;
6479
7038
  return is_likely_component_base_name(base_class);
6480
7039
  }
6481
7040
  }
@@ -6557,6 +7116,13 @@ function addComponent(options) {
6557
7116
  }
6558
7117
  const gameObjectId = gameObjectIdStr;
6559
7118
  const classId = get_class_id(component_type);
7119
+ if (classId === 114 && component_type.toLowerCase() === "monobehaviour") {
7120
+ return {
7121
+ success: false,
7122
+ file_path,
7123
+ error: '"MonoBehaviour" is a base class, not a concrete component script. Provide a script type/path/GUID (for example, "PlayerController", "Assets/Scripts/PlayerController.cs", or a 32-char GUID).'
7124
+ };
7125
+ }
6560
7126
  let duplicateWarning;
6561
7127
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
6562
7128
  if (goBlock && classId !== null) {
@@ -6581,7 +7147,7 @@ function addComponent(options) {
6581
7147
  } else {
6582
7148
  let resolved;
6583
7149
  try {
6584
- resolved = resolve_script_with_fields(component_type, project_path);
7150
+ resolved = resolve_script_with_fields(component_type, project_path, { strict_exact_name: true });
6585
7151
  } catch (e) {
6586
7152
  return {
6587
7153
  success: false,
@@ -6619,13 +7185,20 @@ function addComponent(options) {
6619
7185
  error: `"${component_type}" is ${resolved.kind === "enum" ? "an enum" : "an interface"}, not a MonoBehaviour. Cannot add as a component.`
6620
7186
  };
6621
7187
  }
6622
- if (resolved.base_class && !COMPONENT_BASE_CLASSES.has(resolved.base_class) && !is_valid_component_base(resolved.base_class, project_path)) {
7188
+ if (resolved.is_abstract) {
6623
7189
  return {
6624
7190
  success: false,
6625
7191
  file_path,
6626
- error: `"${component_type}" extends ${resolved.base_class}, which does not inherit from MonoBehaviour. Cannot add as a component. If this is incorrect, ensure the type registry is up to date ("setup" command).`
7192
+ error: `"${component_type}" is abstract and cannot be added as a component.`
6627
7193
  };
6628
7194
  }
7195
+ if (resolved.base_class && !COMPONENT_BASE_CLASSES.has(resolved.base_class) && !is_valid_component_base(resolved.base_class, project_path)) {
7196
+ const warn = `"${component_type}" extends ${resolved.base_class}, which does not inherit from MonoBehaviour. Adding anyway, but ensure the script is a valid component. (Re-run "setup" if the inheritance graph is stale).`;
7197
+ if (!extractionError)
7198
+ extractionError = warn;
7199
+ else
7200
+ extractionError += " " + warn;
7201
+ }
6629
7202
  let version;
6630
7203
  if (project_path) {
6631
7204
  try {
@@ -6640,10 +7213,22 @@ function addComponent(options) {
6640
7213
  componentYAML = createMonoBehaviourYAML(componentId, gameObjectId, resolved.guid, resolved.fields, version, scriptFileId, type_lookup_comp);
6641
7214
  scriptGuid = resolved.guid;
6642
7215
  scriptPath = resolved.path || undefined;
6643
- extractionError = resolved.extraction_error;
7216
+ if (resolved.extraction_error) {
7217
+ if (!extractionError)
7218
+ extractionError = resolved.extraction_error;
7219
+ else
7220
+ extractionError += " " + resolved.extraction_error;
7221
+ }
6644
7222
  }
6645
7223
  if (variantInfo) {
6646
- appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
7224
+ const appendResult = appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
7225
+ if (!appendResult.success) {
7226
+ return {
7227
+ success: false,
7228
+ file_path,
7229
+ error: appendResult.error || `Failed to update m_AddedComponents for PrefabInstance ${variantInfo.prefab_instance_id}`
7230
+ };
7231
+ }
6647
7232
  } else {
6648
7233
  const added = addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
6649
7234
  if (!added) {
@@ -7439,6 +8024,56 @@ function validate_value_type(current_value, new_value) {
7439
8024
  }
7440
8025
  return null;
7441
8026
  }
8027
+ function findStrippedTransformForPrefabInstance(doc, prefabInstanceId) {
8028
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${prefabInstanceId}\\}`);
8029
+ for (const block of doc.blocks) {
8030
+ if ((block.class_id === 4 || block.class_id === 224) && block.is_stripped && piRefPattern.test(block.raw)) {
8031
+ return block;
8032
+ }
8033
+ }
8034
+ return null;
8035
+ }
8036
+ function resolvePrefabRootOrderTarget(prefabInstanceBlock) {
8037
+ const modificationPattern = /- target:\s*(\{[^}]+\})\s*\n\s*propertyPath:\s*('?)([^\n']+)\2/gm;
8038
+ const transformPropertyPattern = /^m_(?:RootOrder|LocalPosition|LocalRotation|LocalScale|ConstrainProportionsScale)(?:\.|$)/;
8039
+ let transformTarget = null;
8040
+ for (const match of prefabInstanceBlock.raw.matchAll(modificationPattern)) {
8041
+ const targetRef = match[1];
8042
+ const propertyPath = match[3].trim();
8043
+ if (propertyPath === "m_RootOrder") {
8044
+ return targetRef;
8045
+ }
8046
+ if (!transformTarget && transformPropertyPattern.test(propertyPath)) {
8047
+ transformTarget = targetRef;
8048
+ }
8049
+ }
8050
+ return transformTarget;
8051
+ }
8052
+ function upsertPrefabRootOrderOverride(prefabInstanceBlock, newRootOrder) {
8053
+ const existingRootOrderPattern = /(- target:\s*\{[^}]+\}\s*\n\s*propertyPath:\s*'?m_RootOrder'?\s*\n\s*value:\s*)([^\n]*)(\s*\n\s*objectReference:\s*\{[^}]+\})/m;
8054
+ if (existingRootOrderPattern.test(prefabInstanceBlock.raw)) {
8055
+ prefabInstanceBlock.replace_raw(prefabInstanceBlock.raw.replace(existingRootOrderPattern, `$1${newRootOrder}$3`));
8056
+ return true;
8057
+ }
8058
+ const targetRef = resolvePrefabRootOrderTarget(prefabInstanceBlock) ?? "{fileID: 400000}";
8059
+ const rootOrderOverride = ` - target: ${targetRef}
8060
+ ` + ` propertyPath: m_RootOrder
8061
+ ` + ` value: ${newRootOrder}
8062
+ ` + " objectReference: {fileID: 0}";
8063
+ const removedPattern = /(\n\s*m_RemovedComponents:)/m;
8064
+ if (removedPattern.test(prefabInstanceBlock.raw)) {
8065
+ prefabInstanceBlock.replace_raw(prefabInstanceBlock.raw.replace(removedPattern, `
8066
+ ${rootOrderOverride}$1`));
8067
+ return true;
8068
+ }
8069
+ const modsPattern = /(m_Modifications:\s*\n)/;
8070
+ if (modsPattern.test(prefabInstanceBlock.raw)) {
8071
+ prefabInstanceBlock.replace_raw(prefabInstanceBlock.raw.replace(modsPattern, `$1${rootOrderOverride}
8072
+ `));
8073
+ return true;
8074
+ }
8075
+ return false;
8076
+ }
7442
8077
  function isAncestor(doc, childTransformId, candidateAncestorTransformId) {
7443
8078
  let currentId = candidateAncestorTransformId;
7444
8079
  const visited = new Set;
@@ -7449,20 +8084,37 @@ function isAncestor(doc, childTransformId, candidateAncestorTransformId) {
7449
8084
  return false;
7450
8085
  visited.add(currentId);
7451
8086
  const block = doc.find_by_file_id(currentId);
7452
- if (!block || block.class_id !== 4)
8087
+ if (!block)
8088
+ break;
8089
+ if (block.class_id === 1001 || block.class_id === 310) {
8090
+ const parentMatch = block.raw.match(/m_TransformParent:\s*\{fileID:\s*(-?\d+)\}/);
8091
+ currentId = parentMatch ? parentMatch[1] : "0";
8092
+ } else if (block.class_id === 4 || block.class_id === 224) {
8093
+ const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
8094
+ currentId = fatherMatch ? fatherMatch[1] : "0";
8095
+ } else {
7453
8096
  break;
7454
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
7455
- currentId = fatherMatch ? fatherMatch[1] : "0";
8097
+ }
7456
8098
  }
7457
8099
  return false;
7458
8100
  }
7459
8101
  function resolveTransformByGameObjectId(doc, gameObjectFileId) {
7460
8102
  const found = doc.find_by_file_id(gameObjectFileId);
7461
- if (!found || found.class_id !== 1) {
7462
- return { error: `GameObject with fileID ${gameObjectFileId} not found` };
8103
+ if (!found) {
8104
+ return { error: `Block with fileID ${gameObjectFileId} not found` };
7463
8105
  }
7464
- const componentMatch = found.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(-?\d+)\}/);
7465
- if (!componentMatch) {
8106
+ if (found.class_id === 4 || found.class_id === 224) {
8107
+ return { id: gameObjectFileId };
8108
+ }
8109
+ if (found.class_id === 1001 || found.class_id === 310) {
8110
+ const strippedTransform = findStrippedTransformForPrefabInstance(doc, gameObjectFileId);
8111
+ return { id: strippedTransform ? strippedTransform.file_id : gameObjectFileId };
8112
+ }
8113
+ if (found.class_id !== 1) {
8114
+ return { error: `fileID ${gameObjectFileId} is not a GameObject, Transform, or PrefabInstance (class ${found.class_id})` };
8115
+ }
8116
+ const componentMatch = found.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(-?\d+)\}/);
8117
+ if (!componentMatch) {
7466
8118
  return { error: `GameObject fileID ${gameObjectFileId} has no Transform component` };
7467
8119
  }
7468
8120
  const transformId = componentMatch[1];
@@ -7615,7 +8267,7 @@ function editComponentByFileId(options) {
7615
8267
  return {
7616
8268
  success: false,
7617
8269
  file_path,
7618
- error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup <project>" first).`
8270
+ error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup" first, or "setup -p <path>").`
7619
8271
  };
7620
8272
  }
7621
8273
  if (resolved !== undefined) {
@@ -7652,7 +8304,7 @@ function editComponentByFileId(options) {
7652
8304
  }
7653
8305
  const targetBlock = doc.find_by_file_id(file_id);
7654
8306
  if (!targetBlock) {
7655
- const hasPrefabInstance = doc.find_by_class_id(1001).length > 0;
8307
+ const hasPrefabInstance = doc.find_by_class_id(1001).length > 0 || doc.find_by_class_id(310).length > 0;
7656
8308
  if (hasPrefabInstance) {
7657
8309
  return {
7658
8310
  success: false,
@@ -8117,10 +8769,26 @@ function reparentGameObject(options) {
8117
8769
  }
8118
8770
  const childBlock = doc.find_by_file_id(childTransformId);
8119
8771
  if (!childBlock) {
8120
- return { success: false, file_path, error: `Transform ${childTransformId} not found` };
8772
+ return { success: false, file_path, error: `Transform or PrefabInstance ${childTransformId} not found` };
8773
+ }
8774
+ const childHierarchyTransformId = (() => {
8775
+ if (childBlock.class_id === 4 || childBlock.class_id === 224) {
8776
+ return childBlock.file_id;
8777
+ }
8778
+ if (childBlock.class_id === 1001 || childBlock.class_id === 310) {
8779
+ const stripped = findStrippedTransformForPrefabInstance(doc, childBlock.file_id);
8780
+ return stripped ? stripped.file_id : null;
8781
+ }
8782
+ return null;
8783
+ })();
8784
+ let oldParentTransformId = "0";
8785
+ if (childBlock.class_id === 1001 || childBlock.class_id === 310) {
8786
+ const parentMatch = childBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(-?\d+)\}/);
8787
+ oldParentTransformId = parentMatch ? parentMatch[1] : "0";
8788
+ } else {
8789
+ const fatherMatch = childBlock.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
8790
+ oldParentTransformId = fatherMatch ? fatherMatch[1] : "0";
8121
8791
  }
8122
- const fatherMatch = childBlock.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
8123
- const oldParentTransformId = fatherMatch ? fatherMatch[1] : "0";
8124
8792
  let newParentTransformId = "0";
8125
8793
  if (new_parent.toLowerCase() !== "root") {
8126
8794
  if (by_id || /^-?\d+$/.test(new_parent)) {
@@ -8147,11 +8815,55 @@ function reparentGameObject(options) {
8147
8815
  }
8148
8816
  }
8149
8817
  if (oldParentTransformId !== "0") {
8150
- doc.remove_child_from_parent(oldParentTransformId, childTransformId);
8818
+ if (childHierarchyTransformId) {
8819
+ doc.remove_child_from_parent(oldParentTransformId, childHierarchyTransformId);
8820
+ } else {
8821
+ doc.remove_child_from_parent(oldParentTransformId, childBlock.file_id);
8822
+ }
8823
+ } else {
8824
+ if (childHierarchyTransformId) {
8825
+ doc.remove_root_from_scene_roots(childHierarchyTransformId);
8826
+ } else {
8827
+ doc.remove_root_from_scene_roots(childBlock.file_id);
8828
+ }
8829
+ }
8830
+ if (childBlock.class_id === 1001 || childBlock.class_id === 310) {
8831
+ const tpPattern = new RegExp(`(m_TransformParent:\\s*)\\{fileID:\\s*(-?\\d+)\\}`);
8832
+ if (tpPattern.test(childBlock.raw)) {
8833
+ let updatedChildRaw = childBlock.raw.replace(tpPattern, `$1{fileID: ${newParentTransformId}}`);
8834
+ childBlock.replace_raw(updatedChildRaw);
8835
+ } else {
8836
+ const modPattern = /(m_Modification:\s*\n)/;
8837
+ const modIndentMatch = childBlock.raw.match(/^([ \t]*)m_Modification:/m);
8838
+ const indent = modIndentMatch ? modIndentMatch[1] : "";
8839
+ let updatedChildRaw = childBlock.raw.replace(modPattern, `$1${indent} m_TransformParent: {fileID: ${newParentTransformId}}
8840
+ `);
8841
+ childBlock.replace_raw(updatedChildRaw);
8842
+ }
8843
+ } else {
8844
+ const fatherPattern = new RegExp(`(m_Father:\\s*)\\{fileID:\\s*\\d+\\}`);
8845
+ if (fatherPattern.test(childBlock.raw)) {
8846
+ let updatedChildRaw = childBlock.raw.replace(fatherPattern, `$1{fileID: ${newParentTransformId}}`);
8847
+ childBlock.replace_raw(updatedChildRaw);
8848
+ } else {
8849
+ const anchorPattern = /^([ \t]*(?:m_GameObject|m_CorrespondingSourceObject|m_PrefabInstance):[ \t]*\{fileID:[ \t]*(-?\d+)[^}]*\}.*)/m;
8850
+ const headerPattern = /(^--- !u!(?:4|224) &-?\d+ stripped\n(?:Rect)?Transform:)/m;
8851
+ if (anchorPattern.test(childBlock.raw)) {
8852
+ let updatedChildRaw = childBlock.raw.replace(anchorPattern, `$1
8853
+ m_Father: {fileID: ${newParentTransformId}}`);
8854
+ childBlock.replace_raw(updatedChildRaw);
8855
+ } else if (headerPattern.test(childBlock.raw)) {
8856
+ let updatedChildRaw = childBlock.raw.replace(headerPattern, `$1
8857
+ m_Father: {fileID: ${newParentTransformId}}`);
8858
+ childBlock.replace_raw(updatedChildRaw);
8859
+ } else {
8860
+ let updatedChildRaw = childBlock.raw.trimEnd() + `
8861
+ m_Father: {fileID: ${newParentTransformId}}
8862
+ `;
8863
+ childBlock.replace_raw(updatedChildRaw);
8864
+ }
8865
+ }
8151
8866
  }
8152
- const fatherPattern = new RegExp(`(m_Father:\\s*)\\{fileID:\\s*\\d+\\}`);
8153
- let updatedChildRaw = childBlock.raw.replace(fatherPattern, `$1{fileID: ${newParentTransformId}}`);
8154
- childBlock.replace_raw(updatedChildRaw);
8155
8867
  {
8156
8868
  let newRootOrder;
8157
8869
  if (newParentTransformId === "0") {
@@ -8159,10 +8871,25 @@ function reparentGameObject(options) {
8159
8871
  } else {
8160
8872
  newRootOrder = doc.calculate_root_order(newParentTransformId);
8161
8873
  }
8162
- childBlock.set_property("m_RootOrder", String(newRootOrder));
8874
+ if (childBlock.class_id === 1001 || childBlock.class_id === 310) {
8875
+ const stripped = doc.blocks.find((b) => (b.class_id === 4 || b.class_id === 224) && b.is_stripped && new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${childTransformId}\\}`).test(b.raw));
8876
+ if (stripped) {
8877
+ stripped.set_property("m_RootOrder", String(newRootOrder));
8878
+ } else {
8879
+ upsertPrefabRootOrderOverride(childBlock, newRootOrder);
8880
+ }
8881
+ } else {
8882
+ childBlock.set_property("m_RootOrder", String(newRootOrder));
8883
+ }
8163
8884
  }
8164
8885
  if (newParentTransformId !== "0") {
8165
- doc.add_child_to_parent(newParentTransformId, childTransformId);
8886
+ if (childHierarchyTransformId) {
8887
+ doc.add_child_to_parent(newParentTransformId, childHierarchyTransformId);
8888
+ }
8889
+ } else {
8890
+ if (childHierarchyTransformId) {
8891
+ doc.add_root_to_scene_roots(childHierarchyTransformId);
8892
+ }
8166
8893
  }
8167
8894
  if (!doc.validate()) {
8168
8895
  return { success: false, file_path, error: "Validation failed after reparent" };
@@ -8181,9 +8908,9 @@ function reparentGameObject(options) {
8181
8908
  }
8182
8909
  function findPrefabInstanceBlock(doc, identifier) {
8183
8910
  const asId = doc.find_by_file_id(identifier);
8184
- if (asId && asId.class_id === 1001)
8911
+ if (asId && (asId.class_id === 1001 || asId.class_id === 310))
8185
8912
  return asId;
8186
- const piBlocks = doc.find_by_class_id(1001);
8913
+ const piBlocks = [...doc.find_by_class_id(1001), ...doc.find_by_class_id(310)];
8187
8914
  const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8188
8915
  const namePattern = new RegExp(`propertyPath:\\s*m_Name\\s+value:\\s*${escaped}\\s`);
8189
8916
  for (const block of piBlocks) {
@@ -8192,6 +8919,193 @@ function findPrefabInstanceBlock(doc, identifier) {
8192
8919
  }
8193
8920
  return null;
8194
8921
  }
8922
+ function parsePrefabRef2(refText) {
8923
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
8924
+ if (!fileIdMatch)
8925
+ return null;
8926
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
8927
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
8928
+ return {
8929
+ file_id: fileIdMatch[1],
8930
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
8931
+ type: typeMatch ? typeMatch[1] : undefined
8932
+ };
8933
+ }
8934
+ function extractPrefabSourceGuid2(prefabInstanceRaw) {
8935
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
8936
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
8937
+ }
8938
+ function normalizePrefabRef2(refText, sourceGuid) {
8939
+ const parsed = parsePrefabRef2(refText);
8940
+ if (!parsed) {
8941
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
8942
+ }
8943
+ const guid = parsed.guid ?? sourceGuid;
8944
+ if (!guid) {
8945
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
8946
+ }
8947
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
8948
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
8949
+ }
8950
+ const type = parsed.type ?? "3";
8951
+ if (!/^-?\d+$/.test(type)) {
8952
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
8953
+ }
8954
+ return {
8955
+ ref: {
8956
+ file_id: parsed.file_id,
8957
+ guid,
8958
+ type
8959
+ }
8960
+ };
8961
+ }
8962
+ function parsePrefabRefMatcher(refText) {
8963
+ const parsed = parsePrefabRef2(refText);
8964
+ if (!parsed) {
8965
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
8966
+ }
8967
+ if (parsed.guid && !/^[a-f0-9]{32}$/i.test(parsed.guid)) {
8968
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
8969
+ }
8970
+ if (parsed.type && !/^-?\d+$/.test(parsed.type)) {
8971
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
8972
+ }
8973
+ return { matcher: parsed };
8974
+ }
8975
+ function prefabRefKey(ref) {
8976
+ return `${ref.file_id}|${ref.guid}|${ref.type}`;
8977
+ }
8978
+ function serializePrefabRef2(ref) {
8979
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
8980
+ }
8981
+ function splitPrefabArraySection2(rawText, key) {
8982
+ const lines = rawText.split(`
8983
+ `);
8984
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
8985
+ let keyIndex = -1;
8986
+ let indent = "";
8987
+ for (let i = 0;i < lines.length; i++) {
8988
+ const match = lines[i].match(keyPattern);
8989
+ if (match) {
8990
+ keyIndex = i;
8991
+ indent = match[1];
8992
+ break;
8993
+ }
8994
+ }
8995
+ if (keyIndex === -1) {
8996
+ return null;
8997
+ }
8998
+ const keyIndentLen = indent.length;
8999
+ let sectionEnd = keyIndex + 1;
9000
+ for (let i = keyIndex + 1;i < lines.length; i++) {
9001
+ const line = lines[i];
9002
+ const trimmed = line.trim();
9003
+ if (trimmed.length === 0) {
9004
+ sectionEnd = i + 1;
9005
+ continue;
9006
+ }
9007
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
9008
+ if (indentLen < keyIndentLen) {
9009
+ break;
9010
+ }
9011
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
9012
+ break;
9013
+ }
9014
+ sectionEnd = i + 1;
9015
+ }
9016
+ return {
9017
+ lines,
9018
+ key_index: keyIndex,
9019
+ section_end: sectionEnd,
9020
+ indent
9021
+ };
9022
+ }
9023
+ function collectNormalizedRefsFromSection(lines, key_index, section_end, sourceGuid) {
9024
+ const sectionText = lines.slice(key_index, section_end).join(`
9025
+ `);
9026
+ const refMatches = sectionText.match(/\{[^}]*\}/g) || [];
9027
+ const dedup = new Map;
9028
+ for (const token of refMatches) {
9029
+ const parsed = parsePrefabRef2(token);
9030
+ if (!parsed)
9031
+ continue;
9032
+ const guid = parsed.guid ?? sourceGuid;
9033
+ if (!guid || !/^[a-f0-9]{32}$/i.test(guid))
9034
+ continue;
9035
+ const type = parsed.type ?? "3";
9036
+ if (!/^-?\d+$/.test(type))
9037
+ continue;
9038
+ const normalized = {
9039
+ file_id: parsed.file_id,
9040
+ guid: guid.toLowerCase(),
9041
+ type
9042
+ };
9043
+ const key = prefabRefKey(normalized);
9044
+ if (!dedup.has(key)) {
9045
+ dedup.set(key, normalized);
9046
+ }
9047
+ }
9048
+ return [...dedup.values()];
9049
+ }
9050
+ function mutatePrefabReferenceList(options) {
9051
+ const { target_block, key, ref_text, action, item_label } = options;
9052
+ const split = splitPrefabArraySection2(target_block.raw, key);
9053
+ if (!split) {
9054
+ return { success: false, error: `${key} property not found in PrefabInstance` };
9055
+ }
9056
+ const sourceGuid = extractPrefabSourceGuid2(target_block.raw);
9057
+ const existing = collectNormalizedRefsFromSection(split.lines, split.key_index, split.section_end, sourceGuid);
9058
+ let next = existing;
9059
+ if (action === "add") {
9060
+ const normalized = normalizePrefabRef2(ref_text, sourceGuid);
9061
+ if (!normalized.ref) {
9062
+ return { success: false, error: normalized.error };
9063
+ }
9064
+ const keyText = prefabRefKey(normalized.ref);
9065
+ if (existing.some((entry) => prefabRefKey(entry) === keyText)) {
9066
+ return { success: true, changed: false };
9067
+ }
9068
+ next = [...existing, normalized.ref];
9069
+ } else {
9070
+ const matcherResult = parsePrefabRefMatcher(ref_text);
9071
+ const matcher = matcherResult.matcher;
9072
+ if (!matcher) {
9073
+ return { success: false, error: matcherResult.error };
9074
+ }
9075
+ next = existing.filter((entry) => {
9076
+ if (entry.file_id !== matcher.file_id)
9077
+ return true;
9078
+ if (matcher.guid && entry.guid !== matcher.guid.toLowerCase())
9079
+ return true;
9080
+ if (matcher.type && entry.type !== matcher.type)
9081
+ return true;
9082
+ return false;
9083
+ });
9084
+ if (next.length === existing.length) {
9085
+ return {
9086
+ success: false,
9087
+ error: `${item_label} reference "${ref_text}" not found in ${key}`
9088
+ };
9089
+ }
9090
+ }
9091
+ const replacement = [];
9092
+ if (next.length === 0) {
9093
+ replacement.push(`${split.indent}${key}: []`);
9094
+ } else {
9095
+ replacement.push(`${split.indent}${key}:`);
9096
+ for (const entry of next) {
9097
+ replacement.push(`${split.indent}- ${serializePrefabRef2(entry)}`);
9098
+ }
9099
+ }
9100
+ const updatedLines = [
9101
+ ...split.lines.slice(0, split.key_index),
9102
+ ...replacement,
9103
+ ...split.lines.slice(split.section_end)
9104
+ ];
9105
+ target_block.replace_raw(updatedLines.join(`
9106
+ `));
9107
+ return { success: true, changed: true };
9108
+ }
8195
9109
  function editArray(options) {
8196
9110
  const { file_path, file_id, array_property, action, value, index } = options;
8197
9111
  if (!import_fs10.existsSync(file_path)) {
@@ -8402,9 +9316,6 @@ function addRemovedComponent(options) {
8402
9316
  if (!import_fs10.existsSync(file_path)) {
8403
9317
  return { success: false, file_path, error: `File not found: ${file_path}` };
8404
9318
  }
8405
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
8406
- return { success: false, file_path, error: `Invalid component reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
8407
- }
8408
9319
  let doc;
8409
9320
  try {
8410
9321
  doc = UnityDocument.from_file(file_path);
@@ -8415,19 +9326,16 @@ function addRemovedComponent(options) {
8415
9326
  if (!targetBlock) {
8416
9327
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8417
9328
  }
8418
- let rawText = targetBlock.raw;
8419
- if (/m_RemovedComponents:\s*\[\]/.test(rawText)) {
8420
- rawText = rawText.replace(/m_RemovedComponents:\s*\[\]/, "m_RemovedComponents:");
8421
- }
8422
- const removedPattern = /m_RemovedComponents:\s*\n/;
8423
- if (removedPattern.test(rawText)) {
8424
- rawText = rawText.replace(removedPattern, `m_RemovedComponents:
8425
- - ${component_ref}
8426
- `);
8427
- } else {
8428
- return { success: false, file_path, error: "m_RemovedComponents property not found in PrefabInstance" };
9329
+ const mutation = mutatePrefabReferenceList({
9330
+ target_block: targetBlock,
9331
+ key: "m_RemovedComponents",
9332
+ ref_text: component_ref,
9333
+ action: "add",
9334
+ item_label: "Component"
9335
+ });
9336
+ if (!mutation.success) {
9337
+ return { success: false, file_path, error: mutation.error };
8429
9338
  }
8430
- targetBlock.replace_raw(rawText);
8431
9339
  if (!doc.validate()) {
8432
9340
  return { success: false, file_path, error: "Validation failed after adding removed component" };
8433
9341
  }
@@ -8456,13 +9364,16 @@ function removeRemovedComponent(options) {
8456
9364
  if (!targetBlock) {
8457
9365
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8458
9366
  }
8459
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8460
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
8461
- if (!refPattern.test(targetBlock.raw)) {
8462
- return { success: false, file_path, error: `Component reference "${component_ref}" not found in m_RemovedComponents` };
9367
+ const mutation = mutatePrefabReferenceList({
9368
+ target_block: targetBlock,
9369
+ key: "m_RemovedComponents",
9370
+ ref_text: component_ref,
9371
+ action: "remove",
9372
+ item_label: "Component"
9373
+ });
9374
+ if (!mutation.success) {
9375
+ return { success: false, file_path, error: mutation.error };
8463
9376
  }
8464
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
8465
- targetBlock.replace_raw(updatedRaw);
8466
9377
  if (!doc.validate()) {
8467
9378
  return { success: false, file_path, error: "Validation failed after removing component" };
8468
9379
  }
@@ -8481,9 +9392,6 @@ function addRemovedGameObject(options) {
8481
9392
  if (!import_fs10.existsSync(file_path)) {
8482
9393
  return { success: false, file_path, error: `File not found: ${file_path}` };
8483
9394
  }
8484
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
8485
- return { success: false, file_path, error: `Invalid GameObject reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
8486
- }
8487
9395
  let doc;
8488
9396
  try {
8489
9397
  doc = UnityDocument.from_file(file_path);
@@ -8494,19 +9402,16 @@ function addRemovedGameObject(options) {
8494
9402
  if (!targetBlock) {
8495
9403
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8496
9404
  }
8497
- let rawText = targetBlock.raw;
8498
- if (/m_RemovedGameObjects:\s*\[\]/.test(rawText)) {
8499
- rawText = rawText.replace(/m_RemovedGameObjects:\s*\[\]/, "m_RemovedGameObjects:");
8500
- }
8501
- const removedPattern = /m_RemovedGameObjects:\s*\n/;
8502
- if (removedPattern.test(rawText)) {
8503
- rawText = rawText.replace(removedPattern, `m_RemovedGameObjects:
8504
- - ${component_ref}
8505
- `);
8506
- } else {
8507
- return { success: false, file_path, error: "m_RemovedGameObjects property not found in PrefabInstance" };
9405
+ const mutation = mutatePrefabReferenceList({
9406
+ target_block: targetBlock,
9407
+ key: "m_RemovedGameObjects",
9408
+ ref_text: component_ref,
9409
+ action: "add",
9410
+ item_label: "GameObject"
9411
+ });
9412
+ if (!mutation.success) {
9413
+ return { success: false, file_path, error: mutation.error };
8508
9414
  }
8509
- targetBlock.replace_raw(rawText);
8510
9415
  if (!doc.validate()) {
8511
9416
  return { success: false, file_path, error: "Validation failed after adding removed GameObject" };
8512
9417
  }
@@ -8535,13 +9440,16 @@ function removeRemovedGameObject(options) {
8535
9440
  if (!targetBlock) {
8536
9441
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8537
9442
  }
8538
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8539
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
8540
- if (!refPattern.test(targetBlock.raw)) {
8541
- return { success: false, file_path, error: `GameObject reference "${component_ref}" not found in m_RemovedGameObjects` };
9443
+ const mutation = mutatePrefabReferenceList({
9444
+ target_block: targetBlock,
9445
+ key: "m_RemovedGameObjects",
9446
+ ref_text: component_ref,
9447
+ action: "remove",
9448
+ item_label: "GameObject"
9449
+ });
9450
+ if (!mutation.success) {
9451
+ return { success: false, file_path, error: mutation.error };
8542
9452
  }
8543
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
8544
- targetBlock.replace_raw(updatedRaw);
8545
9453
  if (!doc.validate()) {
8546
9454
  return { success: false, file_path, error: "Validation failed after removing GameObject" };
8547
9455
  }
@@ -8804,6 +9712,7 @@ ${fieldIndent} - rid: ${nextRid}`;
8804
9712
  }
8805
9713
  // src/editor/delete.ts
8806
9714
  var import_fs13 = require("fs");
9715
+ var import_path6 = require("path");
8807
9716
 
8808
9717
  // src/project-search.ts
8809
9718
  init_scanner();
@@ -9398,6 +10307,7 @@ function removeComponent(options) {
9398
10307
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
9399
10308
  }
9400
10309
  const resolved_file_id = found.file_id;
10310
+ let removed_added_component_overrides = 0;
9401
10311
  const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9402
10312
  if (goMatch) {
9403
10313
  const parentGoId = goMatch[1];
@@ -9408,6 +10318,19 @@ function removeComponent(options) {
9408
10318
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
9409
10319
  if (modifiedRaw === goBlock.raw) {}
9410
10320
  goBlock.replace_raw(modifiedRaw);
10321
+ if (goBlock.is_stripped) {
10322
+ const piMatch = goBlock.raw.match(/m_PrefabInstance:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
10323
+ if (piMatch) {
10324
+ const piBlock = doc.find_by_file_id(piMatch[1]);
10325
+ if (piBlock && piBlock.class_id === 1001) {
10326
+ const updated = removeAddedComponentOverrideEntries(piBlock.raw, resolved_file_id);
10327
+ if (updated.removed_count > 0) {
10328
+ piBlock.replace_raw(updated.updated_raw);
10329
+ removed_added_component_overrides += updated.removed_count;
10330
+ }
10331
+ }
10332
+ }
10333
+ }
9411
10334
  }
9412
10335
  }
9413
10336
  const dangling = [];
@@ -9431,7 +10354,7 @@ function removeComponent(options) {
9431
10354
  file_path,
9432
10355
  removed_file_id: resolved_file_id,
9433
10356
  removed_class_id: found.class_id,
9434
- warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}` : undefined
10357
+ warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}${removed_added_component_overrides > 0 ? ` (removed ${removed_added_component_overrides} m_AddedComponents override entr${removed_added_component_overrides === 1 ? "y" : "ies"})` : ""}` : undefined
9435
10358
  };
9436
10359
  }
9437
10360
  function removeComponentBatch(options) {
@@ -9549,6 +10472,124 @@ function findPrefabInstanceBlock2(doc, identifier) {
9549
10472
  }
9550
10473
  return null;
9551
10474
  }
10475
+ function splitPrefabArraySection3(rawText, key) {
10476
+ const lines = rawText.split(`
10477
+ `);
10478
+ const keyPattern = new RegExp(`^[ ]*${key}:[ ]*(.*)$`);
10479
+ let keyIndex = -1;
10480
+ let keyIndentLen = 0;
10481
+ for (let i = 0;i < lines.length; i++) {
10482
+ const match = lines[i].match(keyPattern);
10483
+ if (match) {
10484
+ keyIndex = i;
10485
+ keyIndentLen = (lines[i].match(/^[ \t]*/) || [""])[0].length;
10486
+ break;
10487
+ }
10488
+ }
10489
+ if (keyIndex === -1) {
10490
+ return null;
10491
+ }
10492
+ let sectionEnd = keyIndex + 1;
10493
+ for (let i = keyIndex + 1;i < lines.length; i++) {
10494
+ const line = lines[i];
10495
+ const trimmed = line.trim();
10496
+ if (trimmed.length === 0) {
10497
+ sectionEnd = i + 1;
10498
+ continue;
10499
+ }
10500
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
10501
+ if (indentLen < keyIndentLen) {
10502
+ break;
10503
+ }
10504
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
10505
+ break;
10506
+ }
10507
+ sectionEnd = i + 1;
10508
+ }
10509
+ return {
10510
+ lines,
10511
+ key_index: keyIndex,
10512
+ section_end: sectionEnd
10513
+ };
10514
+ }
10515
+ function collectAddedObjectFileIdsFromArray(rawText, key) {
10516
+ const section = splitPrefabArraySection3(rawText, key);
10517
+ if (!section)
10518
+ return [];
10519
+ const keyLine = section.lines[section.key_index].trim();
10520
+ if (new RegExp(`^${key}:[ ]*[]$`).test(keyLine)) {
10521
+ return [];
10522
+ }
10523
+ const sectionText = section.lines.slice(section.key_index, section.section_end).join(`
10524
+ `);
10525
+ const fileIds = new Set;
10526
+ const addedObjectPattern = /addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/g;
10527
+ let match;
10528
+ while ((match = addedObjectPattern.exec(sectionText)) !== null) {
10529
+ fileIds.add(match[1]);
10530
+ }
10531
+ if (fileIds.size === 0) {
10532
+ const inlineEntryPattern = /^[ \t]*-[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/gm;
10533
+ while ((match = inlineEntryPattern.exec(sectionText)) !== null) {
10534
+ fileIds.add(match[1]);
10535
+ }
10536
+ }
10537
+ return [...fileIds];
10538
+ }
10539
+ function removeAddedComponentOverrideEntries(rawText, componentId) {
10540
+ const section = splitPrefabArraySection3(rawText, "m_AddedComponents");
10541
+ if (!section)
10542
+ return { updated_raw: rawText, removed_count: 0 };
10543
+ const keyLine = section.lines[section.key_index].trim();
10544
+ if (/^m_AddedComponents:[ \t]*\[\]$/.test(keyLine)) {
10545
+ return { updated_raw: rawText, removed_count: 0 };
10546
+ }
10547
+ const bodyLines = section.lines.slice(section.key_index + 1, section.section_end);
10548
+ const keptEntries = [];
10549
+ let removed = 0;
10550
+ let i = 0;
10551
+ while (i < bodyLines.length) {
10552
+ const line = bodyLines[i];
10553
+ const trimmed = line.trimStart();
10554
+ if (!trimmed.startsWith("-")) {
10555
+ i++;
10556
+ continue;
10557
+ }
10558
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
10559
+ const entryLines = [line];
10560
+ i++;
10561
+ while (i < bodyLines.length) {
10562
+ const nextLine = bodyLines[i];
10563
+ const nextTrimmed = nextLine.trim();
10564
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
10565
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
10566
+ break;
10567
+ }
10568
+ entryLines.push(nextLine);
10569
+ i++;
10570
+ }
10571
+ const entryText = entryLines.join(`
10572
+ `);
10573
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
10574
+ if (addedObjectMatch && addedObjectMatch[1] === componentId) {
10575
+ removed++;
10576
+ continue;
10577
+ }
10578
+ keptEntries.push(entryLines);
10579
+ }
10580
+ if (removed === 0) {
10581
+ return { updated_raw: rawText, removed_count: 0 };
10582
+ }
10583
+ const keyIndent = (section.lines[section.key_index].match(/^[ \t]*/) || [""])[0];
10584
+ const replacement = keptEntries.length === 0 ? [`${keyIndent}m_AddedComponents: []`] : [`${keyIndent}m_AddedComponents:`, ...keptEntries.flat()];
10585
+ const updatedLines = [
10586
+ ...section.lines.slice(0, section.key_index),
10587
+ ...replacement,
10588
+ ...section.lines.slice(section.section_end)
10589
+ ];
10590
+ return { updated_raw: updatedLines.join(`
10591
+ `), removed_count: removed };
10592
+ }
9552
10593
  function deletePrefabInstance(options) {
9553
10594
  const { file_path, prefab_instance } = options;
9554
10595
  const pathError = validate_file_path(file_path, "write");
@@ -9576,36 +10617,23 @@ function deletePrefabInstance(options) {
9576
10617
  allToRemove.add(block.file_id);
9577
10618
  }
9578
10619
  }
9579
- const addedGOPattern = /m_AddedGameObjects:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
9580
- const addedGOMatch = piBlock.raw.match(addedGOPattern);
9581
- if (addedGOMatch) {
9582
- const addedGOSection = addedGOMatch[1];
9583
- const goMatches = addedGOSection.matchAll(/fileID:[ \t]*(\d+)/g);
9584
- for (const match of goMatches) {
9585
- const goId = match[1];
9586
- allToRemove.add(goId);
9587
- const goBlock = doc.find_by_file_id(goId);
9588
- if (goBlock) {
9589
- const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
9590
- if (compMatch) {
9591
- const transformId = compMatch[1];
9592
- allToRemove.add(transformId);
9593
- const descendants = doc.collect_hierarchy(transformId);
9594
- for (const id of descendants) {
9595
- allToRemove.add(id);
9596
- }
10620
+ for (const goId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedGameObjects")) {
10621
+ allToRemove.add(goId);
10622
+ const goBlock = doc.find_by_file_id(goId);
10623
+ if (goBlock) {
10624
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
10625
+ if (compMatch) {
10626
+ const transformId = compMatch[1];
10627
+ allToRemove.add(transformId);
10628
+ const descendants = doc.collect_hierarchy(transformId);
10629
+ for (const id of descendants) {
10630
+ allToRemove.add(id);
9597
10631
  }
9598
10632
  }
9599
10633
  }
9600
10634
  }
9601
- const addedCompPattern = /m_AddedComponents:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
9602
- const addedCompMatch = piBlock.raw.match(addedCompPattern);
9603
- if (addedCompMatch) {
9604
- const addedCompSection = addedCompMatch[1];
9605
- const compMatches = addedCompSection.matchAll(/fileID:[ \t]*(\d+)/g);
9606
- for (const match of compMatches) {
9607
- allToRemove.add(match[1]);
9608
- }
10635
+ for (const componentId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedComponents")) {
10636
+ allToRemove.add(componentId);
9609
10637
  }
9610
10638
  const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
9611
10639
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
@@ -9634,6 +10662,70 @@ function deletePrefabInstance(options) {
9634
10662
  deleted_count: allToRemove.size
9635
10663
  };
9636
10664
  }
10665
+ function deleteAssetFile(options) {
10666
+ const { file_path } = options;
10667
+ const pathError = validate_file_path(file_path, "write");
10668
+ if (pathError) {
10669
+ return { success: false, file_path, error: pathError };
10670
+ }
10671
+ if (!import_fs13.existsSync(file_path)) {
10672
+ return { success: false, file_path, error: `File not found: ${file_path}` };
10673
+ }
10674
+ const supportedExtensions = new Set([
10675
+ ".unity",
10676
+ ".prefab",
10677
+ ".mat",
10678
+ ".anim",
10679
+ ".controller",
10680
+ ".inputactions",
10681
+ ".asset"
10682
+ ]);
10683
+ const extension = import_path6.extname(file_path).toLowerCase();
10684
+ if (!supportedExtensions.has(extension)) {
10685
+ return {
10686
+ success: false,
10687
+ file_path,
10688
+ error: `Unsupported asset type "${extension}". Supported extensions: .unity, .prefab, .mat, .anim, .controller, .inputactions, .asset`
10689
+ };
10690
+ }
10691
+ const metaPath = `${file_path}.meta`;
10692
+ const hasMeta = import_fs13.existsSync(metaPath);
10693
+ try {
10694
+ import_fs13.unlinkSync(file_path);
10695
+ } catch (err) {
10696
+ return {
10697
+ success: false,
10698
+ file_path,
10699
+ error: `Failed to delete file: ${err instanceof Error ? err.message : String(err)}`
10700
+ };
10701
+ }
10702
+ if (hasMeta) {
10703
+ try {
10704
+ import_fs13.unlinkSync(metaPath);
10705
+ return {
10706
+ success: true,
10707
+ file_path,
10708
+ deleted_file: true,
10709
+ deleted_meta: true
10710
+ };
10711
+ } catch (err) {
10712
+ return {
10713
+ success: true,
10714
+ file_path,
10715
+ deleted_file: true,
10716
+ deleted_meta: false,
10717
+ warning: `Deleted asset file but failed to delete .meta file: ${err instanceof Error ? err.message : String(err)}`
10718
+ };
10719
+ }
10720
+ }
10721
+ return {
10722
+ success: true,
10723
+ file_path,
10724
+ deleted_file: true,
10725
+ deleted_meta: false,
10726
+ warning: `Asset file deleted, but no .meta file found at ${metaPath}`
10727
+ };
10728
+ }
9637
10729
  // src/editor/duplicate.ts
9638
10730
  var import_fs14 = require("fs");
9639
10731
  var path6 = __toESM(require("path"));
@@ -10278,9 +11370,9 @@ function move_scene(projectPath, scenePath, newPosition) {
10278
11370
 
10279
11371
  // src/packages.ts
10280
11372
  var import_fs15 = require("fs");
10281
- var import_path6 = require("path");
11373
+ var import_path7 = require("path");
10282
11374
  function load_manifest(project_path) {
10283
- const manifest_path = import_path6.join(project_path, "Packages", "manifest.json");
11375
+ const manifest_path = import_path7.join(project_path, "Packages", "manifest.json");
10284
11376
  if (!import_fs15.existsSync(manifest_path)) {
10285
11377
  return { error: `manifest.json not found at ${manifest_path}` };
10286
11378
  }
@@ -10474,24 +11566,381 @@ function remove_control_scheme(data, name) {
10474
11566
  return data;
10475
11567
  }
10476
11568
 
10477
- // src/cmd-create.ts
10478
- function build_create_command() {
10479
- const cmd = new Command("create").description("Create Unity objects (GameObjects, scenes, prefabs, components)");
10480
- cmd.command("gameobject <file> [name]").description("Create a new GameObject in a Unity file").option("-p, --parent <name|id>", "Parent GameObject name or Transform fileID").option("-n, --name <name>", "GameObject name (alternative to positional arg)").option("-j, --json", "Output as JSON").action((file, name_positional, options) => {
10481
- const name = name_positional || options.name;
10482
- if (!name) {
10483
- console.log(JSON.stringify({
10484
- success: false,
10485
- error: "Missing required name. Provide as positional argument or --name flag.",
10486
- correct_usage: [
10487
- "unity-agentic-tools create gameobject <file> <name>",
10488
- "unity-agentic-tools create gameobject <file> --name <name>"
10489
- ]
10490
- }, null, 2));
10491
- process.exitCode = 1;
10492
- return;
11569
+ // src/loaded-protection.ts
11570
+ var import_path9 = require("path");
11571
+
11572
+ // src/editor-client.ts
11573
+ var import_fs17 = require("fs");
11574
+ var import_path8 = require("path");
11575
+ function read_editor_config(project_path) {
11576
+ const config_path = import_path8.join(project_path, ".unity-agentic", "editor.json");
11577
+ if (!import_fs17.existsSync(config_path)) {
11578
+ return { error: `Editor bridge not found at ${config_path}. Is the Unity Editor running with the bridge package installed?` };
11579
+ }
11580
+ let config;
11581
+ try {
11582
+ const raw = import_fs17.readFileSync(config_path, "utf-8");
11583
+ config = JSON.parse(raw);
11584
+ } catch (err) {
11585
+ return { error: `Failed to parse editor.json: ${err instanceof Error ? err.message : String(err)}` };
11586
+ }
11587
+ if (typeof config.port !== "number" || typeof config.pid !== "number") {
11588
+ return { error: "Invalid editor.json: missing port or pid" };
11589
+ }
11590
+ if (!is_pid_alive(config.pid)) {
11591
+ return { error: `Unity Editor process (PID ${config.pid}) is not running. The editor may have been closed.` };
11592
+ }
11593
+ return config;
11594
+ }
11595
+ var RETRYABLE_CODES = new Set([-32000, -32002, -32003]);
11596
+ var RETRY_DELAYS = [500, 1000, 2000];
11597
+ async function call_editor(options) {
11598
+ const maxRetries = options.retries ?? 2;
11599
+ let lastResponse;
11600
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
11601
+ lastResponse = await call_editor_once(options);
11602
+ if (!lastResponse.error || !RETRYABLE_CODES.has(lastResponse.error.code)) {
11603
+ return lastResponse;
10493
11604
  }
10494
- const ext = file.toLowerCase().split(".").pop();
11605
+ if (attempt < maxRetries) {
11606
+ const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)];
11607
+ await new Promise((r) => setTimeout(r, delay));
11608
+ }
11609
+ }
11610
+ return lastResponse;
11611
+ }
11612
+ function call_editor_once(options) {
11613
+ const { method, params, timeout = 1e4, no_wait } = options;
11614
+ const config = resolve_config(options);
11615
+ if ("error" in config) {
11616
+ return Promise.resolve({
11617
+ jsonrpc: "2.0",
11618
+ id: "0",
11619
+ error: { code: -32000, message: config.error }
11620
+ });
11621
+ }
11622
+ const url = `ws://127.0.0.1:${config.port}/unity-agentic`;
11623
+ const request_id = generate_id();
11624
+ const wire_params = { ...params };
11625
+ if (timeout !== 1e4)
11626
+ wire_params._timeout = timeout;
11627
+ if (no_wait)
11628
+ wire_params.no_wait = true;
11629
+ const request = {
11630
+ jsonrpc: "2.0",
11631
+ id: request_id,
11632
+ method,
11633
+ ...Object.keys(wire_params).length > 0 ? { params: wire_params } : {}
11634
+ };
11635
+ return new Promise((resolve5) => {
11636
+ let resolved = false;
11637
+ let ws;
11638
+ const timer = setTimeout(() => {
11639
+ if (!resolved) {
11640
+ resolved = true;
11641
+ try {
11642
+ ws?.close();
11643
+ } catch {}
11644
+ resolve5({
11645
+ jsonrpc: "2.0",
11646
+ id: request_id,
11647
+ error: { code: -32001, message: `Timeout after ${timeout}ms waiting for response to ${method}` }
11648
+ });
11649
+ }
11650
+ }, timeout);
11651
+ try {
11652
+ ws = new WebSocket(url);
11653
+ ws.onopen = () => {
11654
+ ws.send(JSON.stringify(request));
11655
+ if (no_wait && !resolved) {
11656
+ resolved = true;
11657
+ clearTimeout(timer);
11658
+ setTimeout(() => {
11659
+ try {
11660
+ ws.close();
11661
+ } catch {}
11662
+ }, 200);
11663
+ resolve5({
11664
+ jsonrpc: "2.0",
11665
+ id: request_id,
11666
+ result: { queued: true }
11667
+ });
11668
+ }
11669
+ };
11670
+ ws.onmessage = (event) => {
11671
+ try {
11672
+ const data = JSON.parse(String(event.data));
11673
+ if (data.id === request_id) {
11674
+ if (!resolved) {
11675
+ resolved = true;
11676
+ clearTimeout(timer);
11677
+ ws.close();
11678
+ resolve5(data);
11679
+ }
11680
+ }
11681
+ } catch {}
11682
+ };
11683
+ ws.onerror = () => {
11684
+ if (!resolved) {
11685
+ resolved = true;
11686
+ clearTimeout(timer);
11687
+ resolve5({
11688
+ jsonrpc: "2.0",
11689
+ id: request_id,
11690
+ error: { code: -32002, message: `WebSocket connection failed to ${url}. Is the Unity Editor running?` }
11691
+ });
11692
+ }
11693
+ };
11694
+ ws.onclose = () => {
11695
+ if (!resolved) {
11696
+ resolved = true;
11697
+ clearTimeout(timer);
11698
+ resolve5({
11699
+ jsonrpc: "2.0",
11700
+ id: request_id,
11701
+ error: { code: -32003, message: "WebSocket connection closed before response received" }
11702
+ });
11703
+ }
11704
+ };
11705
+ } catch (err) {
11706
+ if (!resolved) {
11707
+ resolved = true;
11708
+ clearTimeout(timer);
11709
+ resolve5({
11710
+ jsonrpc: "2.0",
11711
+ id: request_id,
11712
+ error: { code: -32002, message: `Failed to connect: ${err instanceof Error ? err.message : String(err)}` }
11713
+ });
11714
+ }
11715
+ }
11716
+ });
11717
+ }
11718
+ async function stream_editor(options) {
11719
+ const { method, params, timeout = 30000, on_event } = options;
11720
+ const config = resolve_config(options);
11721
+ if ("error" in config) {
11722
+ throw new Error(config.error);
11723
+ }
11724
+ const MAX_RECONNECTS = 5;
11725
+ let reconnect_count = 0;
11726
+ let stopped = false;
11727
+ return new Promise((resolve5, reject) => {
11728
+ let resolved = false;
11729
+ function connect(url) {
11730
+ const ws = new WebSocket(url);
11731
+ const request_id = generate_id();
11732
+ const request = {
11733
+ jsonrpc: "2.0",
11734
+ id: request_id,
11735
+ method,
11736
+ ...params ? { params } : {}
11737
+ };
11738
+ let connected = false;
11739
+ const timer = !resolved ? setTimeout(() => {
11740
+ if (!connected && !resolved) {
11741
+ reject(new Error(`Timeout connecting to ${url}`));
11742
+ }
11743
+ }, timeout) : null;
11744
+ ws.onopen = () => {
11745
+ connected = true;
11746
+ if (timer)
11747
+ clearTimeout(timer);
11748
+ reconnect_count = 0;
11749
+ ws.send(JSON.stringify(request));
11750
+ if (!resolved) {
11751
+ resolved = true;
11752
+ resolve5({ close: () => {
11753
+ stopped = true;
11754
+ ws.close();
11755
+ } });
11756
+ }
11757
+ };
11758
+ ws.onmessage = (event) => {
11759
+ try {
11760
+ const data = JSON.parse(String(event.data));
11761
+ if ("method" in data && !("id" in data)) {
11762
+ on_event(data);
11763
+ }
11764
+ } catch {}
11765
+ };
11766
+ ws.onerror = () => {
11767
+ if (!connected && !resolved) {
11768
+ if (timer)
11769
+ clearTimeout(timer);
11770
+ reject(new Error(`WebSocket connection failed to ${url}`));
11771
+ }
11772
+ };
11773
+ ws.onclose = () => {
11774
+ if (stopped)
11775
+ return;
11776
+ if (!connected && !resolved)
11777
+ return;
11778
+ if (reconnect_count >= MAX_RECONNECTS)
11779
+ return;
11780
+ reconnect_count++;
11781
+ const delay = Math.min(500 * reconnect_count, 3000);
11782
+ setTimeout(() => {
11783
+ if (stopped)
11784
+ return;
11785
+ const fresh_config = resolve_config(options);
11786
+ if ("error" in fresh_config)
11787
+ return;
11788
+ connect(`ws://127.0.0.1:${fresh_config.port}/unity-agentic`);
11789
+ }, delay);
11790
+ };
11791
+ }
11792
+ connect(`ws://127.0.0.1:${config.port}/unity-agentic`);
11793
+ });
11794
+ }
11795
+ async function ping_editor(port, timeout_ms = 2000) {
11796
+ return new Promise((resolve5) => {
11797
+ const timer = setTimeout(() => {
11798
+ resolve5({ reachable: false, error: `Timeout after ${timeout_ms}ms` });
11799
+ }, timeout_ms);
11800
+ try {
11801
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/unity-agentic`);
11802
+ ws.onopen = () => {
11803
+ clearTimeout(timer);
11804
+ try {
11805
+ ws.close();
11806
+ } catch {}
11807
+ resolve5({ reachable: true });
11808
+ };
11809
+ ws.onerror = (err) => {
11810
+ clearTimeout(timer);
11811
+ resolve5({ reachable: false, error: String(err) });
11812
+ };
11813
+ } catch (err) {
11814
+ clearTimeout(timer);
11815
+ resolve5({ reachable: false, error: String(err) });
11816
+ }
11817
+ });
11818
+ }
11819
+ function resolve_config(options) {
11820
+ if (options.port) {
11821
+ return { port: options.port, pid: 0, version: "unknown" };
11822
+ }
11823
+ return read_editor_config(options.project_path);
11824
+ }
11825
+ function is_pid_alive(pid) {
11826
+ try {
11827
+ process.kill(pid, 0);
11828
+ return true;
11829
+ } catch {
11830
+ return false;
11831
+ }
11832
+ }
11833
+ function generate_id() {
11834
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
11835
+ }
11836
+
11837
+ // src/loaded-protection.ts
11838
+ function normalize_slashes(value) {
11839
+ return value.replace(/\\/g, "/");
11840
+ }
11841
+ function to_project_asset_path(file_path, project_path) {
11842
+ const abs_file = import_path9.resolve(file_path);
11843
+ const abs_project = import_path9.resolve(project_path);
11844
+ const rel = import_path9.relative(abs_project, abs_file);
11845
+ if (!rel || rel.startsWith("..") || rel.includes(`..${import_path9.sep}`)) {
11846
+ if (/^Assets[\\/]/.test(file_path)) {
11847
+ return normalize_slashes(file_path);
11848
+ }
11849
+ return null;
11850
+ }
11851
+ return normalize_slashes(rel);
11852
+ }
11853
+ function normalize_loaded_paths(paths) {
11854
+ return new Set(paths.map((p) => normalize_slashes(p)));
11855
+ }
11856
+ async function get_loaded_state(project_path) {
11857
+ const config = read_editor_config(project_path);
11858
+ if ("error" in config)
11859
+ return null;
11860
+ const ping = await ping_editor(config.port, 1200);
11861
+ if (!ping.reachable)
11862
+ return null;
11863
+ const response = await call_editor({
11864
+ project_path,
11865
+ method: "editor.scene.loaded",
11866
+ timeout: 2500
11867
+ });
11868
+ if (response.error || !response.result || typeof response.result !== "object") {
11869
+ return null;
11870
+ }
11871
+ const result = response.result;
11872
+ const loaded = Array.isArray(result.loaded_scene_paths) ? result.loaded_scene_paths.filter((v) => typeof v === "string") : [];
11873
+ return {
11874
+ loaded_scene_paths: loaded,
11875
+ active_scene_path: typeof result.active_scene_path === "string" ? result.active_scene_path : undefined,
11876
+ prefab_stage_path: typeof result.prefab_stage_path === "string" ? result.prefab_stage_path : undefined
11877
+ };
11878
+ }
11879
+ async function is_editor_connected_for_project(project_path) {
11880
+ const resolved_project = resolve_project_path(project_path);
11881
+ const config = read_editor_config(resolved_project);
11882
+ if ("error" in config)
11883
+ return false;
11884
+ const ping = await ping_editor(config.port, 1200);
11885
+ return ping.reachable;
11886
+ }
11887
+ async function enforce_loaded_edit_protection(file_path, bypass, project_path) {
11888
+ if (bypass)
11889
+ return { allowed: true };
11890
+ const ext = file_path.toLowerCase().split(".").pop();
11891
+ if (!ext || ext !== "unity" && ext !== "prefab") {
11892
+ return { allowed: true };
11893
+ }
11894
+ const inferred_project = project_path ? resolve_project_path(project_path) : find_unity_project_root(import_path9.dirname(import_path9.resolve(file_path))) || process.cwd();
11895
+ const loaded_state = await get_loaded_state(inferred_project);
11896
+ if (!loaded_state) {
11897
+ return { allowed: true };
11898
+ }
11899
+ const target_asset_path = to_project_asset_path(file_path, inferred_project);
11900
+ if (!target_asset_path) {
11901
+ return { allowed: true };
11902
+ }
11903
+ const loaded_scenes = normalize_loaded_paths(loaded_state.loaded_scene_paths);
11904
+ const active_scene = loaded_state.active_scene_path ? normalize_slashes(loaded_state.active_scene_path) : "";
11905
+ const prefab_stage = loaded_state.prefab_stage_path ? normalize_slashes(loaded_state.prefab_stage_path) : "";
11906
+ if (target_asset_path.endsWith(".unity")) {
11907
+ if (loaded_scenes.has(target_asset_path)) {
11908
+ const reason = active_scene === target_asset_path ? "it is currently the active scene in the Unity Editor" : "it is currently loaded in the Unity Editor";
11909
+ return {
11910
+ allowed: false,
11911
+ error: `Refusing to edit ${target_asset_path}: ${reason}. Use --bypass-loaded-protection to force file-based edits while editor is connected.`
11912
+ };
11913
+ }
11914
+ }
11915
+ if (target_asset_path.endsWith(".prefab")) {
11916
+ if (prefab_stage === target_asset_path) {
11917
+ return {
11918
+ allowed: false,
11919
+ error: `Refusing to edit ${target_asset_path}: it is currently open in Prefab Mode. Use --bypass-loaded-protection to force file-based edits while editor is connected.`
11920
+ };
11921
+ }
11922
+ }
11923
+ return { allowed: true };
11924
+ }
11925
+
11926
+ // src/cmd-create.ts
11927
+ function build_create_command() {
11928
+ const cmd = new Command("create").description("Create Unity objects (GameObjects, scenes, prefabs, components)");
11929
+ cmd.command("gameobject <file> [name]").description("Create a new GameObject in a Unity file").option("-p, --parent <name|id>", "Parent GameObject name or Transform fileID").option("-n, --name <name>", "GameObject name (alternative to positional arg)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, name_positional, options) => {
11930
+ const name = name_positional || options.name;
11931
+ if (!name) {
11932
+ console.log(JSON.stringify({
11933
+ success: false,
11934
+ error: "Missing required name. Provide as positional argument or --name flag.",
11935
+ correct_usage: [
11936
+ "unity-agentic-tools create gameobject <file> <name>",
11937
+ "unity-agentic-tools create gameobject <file> --name <name>"
11938
+ ]
11939
+ }, null, 2));
11940
+ process.exitCode = 1;
11941
+ return;
11942
+ }
11943
+ const ext = file.toLowerCase().split(".").pop();
10495
11944
  if (!ext || !["unity", "prefab", "asset"].includes(ext)) {
10496
11945
  console.log(JSON.stringify({ success: false, error: `Invalid file type ".${ext}". create gameobject only works with .unity, .prefab, or .asset files` }, null, 2));
10497
11946
  process.exitCode = 1;
@@ -10502,6 +11951,12 @@ function build_create_command() {
10502
11951
  const asNumber = parseInt(options.parent, 10);
10503
11952
  parent = isNaN(asNumber) ? options.parent : asNumber;
10504
11953
  }
11954
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
11955
+ if (!guard.allowed) {
11956
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
11957
+ process.exitCode = 1;
11958
+ return;
11959
+ }
10505
11960
  const result = createGameObject({
10506
11961
  file_path: file,
10507
11962
  name,
@@ -10530,7 +11985,7 @@ function build_create_command() {
10530
11985
  if (!result.success)
10531
11986
  process.exitCode = 1;
10532
11987
  });
10533
- cmd.command("prefab-instance <scene_file> <prefab_path>").description("Instantiate a prefab into a scene file").option("-n, --name <name>", "Instance name (defaults to prefab filename)").option("-p, --parent <name|id>", "Parent GameObject name or Transform fileID").option("--position <x,y,z>", "Local position (default: 0,0,0)").option("-j, --json", "Output as JSON").action((scene_file, prefab_path_arg, options) => {
11988
+ cmd.command("prefab-instance <scene_file> <prefab_path>").description("Instantiate a prefab into a scene file").option("-n, --name <name>", "Instance name (defaults to prefab filename)").option("-p, --parent <name|id>", "Parent GameObject name or Transform fileID").option("--position <x,y,z>", "Local position (default: 0,0,0)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (scene_file, prefab_path_arg, options) => {
10534
11989
  let position;
10535
11990
  if (options.position) {
10536
11991
  const parts = options.position.split(",").map(Number);
@@ -10549,6 +12004,12 @@ function build_create_command() {
10549
12004
  const asNumber = parseInt(options.parent, 10);
10550
12005
  parent = isNaN(asNumber) ? options.parent : asNumber;
10551
12006
  }
12007
+ const guard = await enforce_loaded_edit_protection(scene_file, options.bypassLoadedProtection);
12008
+ if (!guard.allowed) {
12009
+ console.log(JSON.stringify({ success: false, file_path: scene_file, error: guard.error }, null, 2));
12010
+ process.exitCode = 1;
12011
+ return;
12012
+ }
10552
12013
  const result = createPrefabInstance({
10553
12014
  scene_path: scene_file,
10554
12015
  prefab_path: prefab_path_arg,
@@ -10589,12 +12050,18 @@ function build_create_command() {
10589
12050
  if (!result.success)
10590
12051
  process.exitCode = 1;
10591
12052
  });
10592
- cmd.command("component <file> <object_name> <component>").description("Add a Unity component (e.g., MeshRenderer, Animator, Rigidbody) or custom script").option("-p, --project <path>", "Unity project path (for script GUID lookup)").option("-j, --json", "Output as JSON").action((file, object_name, component, options) => {
12053
+ cmd.command("component <file> <object_name> <component>").description("Add a Unity component (e.g., MeshRenderer, Animator, Rigidbody) or custom script").option("-p, --project <path>", "Unity project path (for script GUID lookup)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, object_name, component, options) => {
10593
12054
  if (!component || component.trim() === "") {
10594
12055
  console.log(JSON.stringify({ success: false, file_path: file, error: "Component name must not be empty" }, null, 2));
10595
12056
  process.exitCode = 1;
10596
12057
  return;
10597
12058
  }
12059
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection, options.project);
12060
+ if (!guard.allowed) {
12061
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
12062
+ process.exitCode = 1;
12063
+ return;
12064
+ }
10598
12065
  const result = addComponent({
10599
12066
  file_path: file,
10600
12067
  game_object_name: object_name,
@@ -10605,7 +12072,13 @@ function build_create_command() {
10605
12072
  if (!result.success)
10606
12073
  process.exitCode = 1;
10607
12074
  });
10608
- cmd.command("component-copy <file> <source_file_id> <target_object_name>").description("Copy a component to a target GameObject").option("-j, --json", "Output as JSON").action((file, source_file_id, target_object_name, _options) => {
12075
+ cmd.command("component-copy <file> <source_file_id> <target_object_name>").description("Copy a component to a target GameObject").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, source_file_id, target_object_name, options) => {
12076
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
12077
+ if (!guard.allowed) {
12078
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
12079
+ process.exitCode = 1;
12080
+ return;
12081
+ }
10609
12082
  const result = copyComponent({
10610
12083
  file_path: file,
10611
12084
  source_file_id,
@@ -10615,10 +12088,11 @@ function build_create_command() {
10615
12088
  if (!result.success)
10616
12089
  process.exitCode = 1;
10617
12090
  });
10618
- cmd.command("build <project_path> <scene_path>").description("Add a scene to build settings").option("--index <n>", "Insert at position (0-based)").option("-j, --json", "Output as JSON").action((project_path, scene_path, options) => {
12091
+ cmd.command("build <scene_path>").description("Add a scene to build settings").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--index <n>", "Insert at position (0-based)").option("-j, --json", "Output as JSON").action((scene_path, options) => {
10619
12092
  try {
12093
+ const resolvedProjectPath = resolve_project_path(options.project);
10620
12094
  const position = options.index !== undefined ? parseInt(options.index, 10) : undefined;
10621
- const result = add_scene(project_path, scene_path, { position });
12095
+ const result = add_scene(resolvedProjectPath, scene_path, { position });
10622
12096
  console.log(JSON.stringify(result, null, 2));
10623
12097
  if (!result.success)
10624
12098
  process.exitCode = 1;
@@ -10632,7 +12106,7 @@ function build_create_command() {
10632
12106
  console.log(JSON.stringify({ success: false, error: "--shader <guid> is required" }, null, 2));
10633
12107
  process.exit(1);
10634
12108
  }
10635
- if (import_fs17.existsSync(output_path)) {
12109
+ if (import_fs18.existsSync(output_path)) {
10636
12110
  console.log(JSON.stringify({ success: false, error: `File already exists: ${output_path}` }, null, 2));
10637
12111
  process.exit(1);
10638
12112
  }
@@ -10705,7 +12179,7 @@ Material:
10705
12179
  m_BuildTextureStacks: []
10706
12180
  `;
10707
12181
  ensure_parent_dir(output_path);
10708
- import_fs17.writeFileSync(output_path, mat_content, "utf-8");
12182
+ import_fs18.writeFileSync(output_path, mat_content, "utf-8");
10709
12183
  const guid = import_crypto2.randomBytes(16).toString("hex");
10710
12184
  const meta_content = `fileFormatVersion: 2
10711
12185
  guid: ${guid}
@@ -10716,7 +12190,7 @@ NativeFormatImporter:
10716
12190
  assetBundleName:
10717
12191
  assetBundleVariant:
10718
12192
  `;
10719
- import_fs17.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
12193
+ import_fs18.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
10720
12194
  console.log(JSON.stringify({
10721
12195
  success: true,
10722
12196
  file: output_path,
@@ -10726,9 +12200,10 @@ NativeFormatImporter:
10726
12200
  shader_guid
10727
12201
  }, null, 2));
10728
12202
  });
10729
- cmd.command("package <project_path> <name> <version>").description("Add a package to Packages/manifest.json").option("-j, --json", "Output as JSON").action((project_path, name, version, _options) => {
12203
+ cmd.command("package <name> <version>").description("Add a package to Packages/manifest.json").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((name, version, options) => {
10730
12204
  try {
10731
- const result = add_package(project_path, name, version);
12205
+ const resolvedProjectPath = resolve_project_path(options.project);
12206
+ const result = add_package(resolvedProjectPath, name, version);
10732
12207
  if ("error" in result) {
10733
12208
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
10734
12209
  process.exitCode = 1;
@@ -10746,7 +12221,7 @@ NativeFormatImporter:
10746
12221
  process.exitCode = 1;
10747
12222
  return;
10748
12223
  }
10749
- if (import_fs17.existsSync(output_path)) {
12224
+ if (import_fs18.existsSync(output_path)) {
10750
12225
  console.log(JSON.stringify({ success: false, error: `File already exists: ${output_path}` }, null, 2));
10751
12226
  process.exitCode = 1;
10752
12227
  return;
@@ -10773,7 +12248,7 @@ ScriptedImporter:
10773
12248
  wrapperClassName:
10774
12249
  wrapperCodeNamespace:
10775
12250
  `;
10776
- import_fs17.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
12251
+ import_fs18.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
10777
12252
  console.log(JSON.stringify({
10778
12253
  success: true,
10779
12254
  file: output_path,
@@ -10788,8 +12263,8 @@ ScriptedImporter:
10788
12263
  process.exitCode = 1;
10789
12264
  return;
10790
12265
  }
10791
- const name = name_arg || import_path7.basename(output_path).replace(/\.anim$/i, "");
10792
- if (import_fs17.existsSync(output_path)) {
12266
+ const name = name_arg || import_path10.basename(output_path).replace(/\.anim$/i, "");
12267
+ if (import_fs18.existsSync(output_path)) {
10793
12268
  console.log(JSON.stringify({ success: false, error: `File already exists: ${output_path}` }, null, 2));
10794
12269
  process.exitCode = 1;
10795
12270
  return;
@@ -10857,7 +12332,7 @@ AnimationClip:
10857
12332
  m_Events: []
10858
12333
  `;
10859
12334
  ensure_parent_dir(output_path);
10860
- import_fs17.writeFileSync(output_path, anim_content, "utf-8");
12335
+ import_fs18.writeFileSync(output_path, anim_content, "utf-8");
10861
12336
  const guid = import_crypto2.randomBytes(16).toString("hex");
10862
12337
  const meta_content = `fileFormatVersion: 2
10863
12338
  guid: ${guid}
@@ -10868,7 +12343,7 @@ NativeFormatImporter:
10868
12343
  assetBundleName:
10869
12344
  assetBundleVariant:
10870
12345
  `;
10871
- import_fs17.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
12346
+ import_fs18.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
10872
12347
  console.log(JSON.stringify({
10873
12348
  success: true,
10874
12349
  file: output_path,
@@ -10880,13 +12355,13 @@ NativeFormatImporter:
10880
12355
  }, null, 2));
10881
12356
  });
10882
12357
  cmd.command("animator <output_path> [name]").description("Create a blank .controller AnimatorController file (name defaults to filename without extension)").option("--layer <name>", 'Name of the initial layer (default: "Base Layer")', "Base Layer").option("-j, --json", "Output as JSON").action((output_path, name_arg, options) => {
10883
- const name = name_arg || import_path7.basename(output_path).replace(/\.controller$/i, "");
12358
+ const name = name_arg || import_path10.basename(output_path).replace(/\.controller$/i, "");
10884
12359
  if (!output_path.toLowerCase().endsWith(".controller")) {
10885
12360
  console.log(JSON.stringify({ success: false, error: "Output path must end with .controller" }, null, 2));
10886
12361
  process.exitCode = 1;
10887
12362
  return;
10888
12363
  }
10889
- if (import_fs17.existsSync(output_path)) {
12364
+ if (import_fs18.existsSync(output_path)) {
10890
12365
  console.log(JSON.stringify({ success: false, error: `File already exists: ${output_path}` }, null, 2));
10891
12366
  process.exitCode = 1;
10892
12367
  return;
@@ -10937,7 +12412,7 @@ AnimatorStateMachine:
10937
12412
  m_DefaultState: {fileID: 0}
10938
12413
  `;
10939
12414
  ensure_parent_dir(output_path);
10940
- import_fs17.writeFileSync(output_path, ctrl_content, "utf-8");
12415
+ import_fs18.writeFileSync(output_path, ctrl_content, "utf-8");
10941
12416
  const guid = import_crypto2.randomBytes(16).toString("hex");
10942
12417
  const meta_content = `fileFormatVersion: 2
10943
12418
  guid: ${guid}
@@ -10948,7 +12423,7 @@ NativeFormatImporter:
10948
12423
  assetBundleName:
10949
12424
  assetBundleVariant:
10950
12425
  `;
10951
- import_fs17.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
12426
+ import_fs18.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
10952
12427
  console.log(JSON.stringify({
10953
12428
  success: true,
10954
12429
  file: output_path,
@@ -10959,13 +12434,13 @@ NativeFormatImporter:
10959
12434
  }, null, 2));
10960
12435
  });
10961
12436
  cmd.command("prefab <output_path> [name]").description("Create a blank .prefab file (name defaults to filename without extension)").option("-j, --json", "Output as JSON").action((output_path, name_arg) => {
10962
- const name = name_arg || import_path7.basename(output_path).replace(/\.prefab$/i, "");
12437
+ const name = name_arg || import_path10.basename(output_path).replace(/\.prefab$/i, "");
10963
12438
  if (!output_path.toLowerCase().endsWith(".prefab")) {
10964
12439
  console.log(JSON.stringify({ success: false, error: "Output path must end with .prefab" }, null, 2));
10965
12440
  process.exitCode = 1;
10966
12441
  return;
10967
12442
  }
10968
- if (import_fs17.existsSync(output_path)) {
12443
+ if (import_fs18.existsSync(output_path)) {
10969
12444
  console.log(JSON.stringify({ success: false, error: `File already exists: ${output_path}` }, null, 2));
10970
12445
  process.exitCode = 1;
10971
12446
  return;
@@ -11005,7 +12480,7 @@ Transform:
11005
12480
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
11006
12481
  `;
11007
12482
  ensure_parent_dir(output_path);
11008
- import_fs17.writeFileSync(output_path, prefab_content, "utf-8");
12483
+ import_fs18.writeFileSync(output_path, prefab_content, "utf-8");
11009
12484
  const guid = import_crypto2.randomBytes(16).toString("hex");
11010
12485
  const meta_content = `fileFormatVersion: 2
11011
12486
  guid: ${guid}
@@ -11015,7 +12490,7 @@ PrefabImporter:
11015
12490
  assetBundleName:
11016
12491
  assetBundleVariant:
11017
12492
  `;
11018
- import_fs17.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
12493
+ import_fs18.writeFileSync(`${output_path}.meta`, meta_content, "utf-8");
11019
12494
  console.log(JSON.stringify({
11020
12495
  success: true,
11021
12496
  file: output_path,
@@ -11029,9 +12504,119 @@ PrefabImporter:
11029
12504
 
11030
12505
  // src/cmd-read.ts
11031
12506
  init_scanner();
11032
- var import_fs18 = require("fs");
11033
- var import_path8 = require("path");
12507
+ var import_fs19 = require("fs");
12508
+ var import_path11 = require("path");
11034
12509
  var import_os = require("os");
12510
+ function parse_unity_ref(ref_text) {
12511
+ const file_id_match = ref_text.match(/fileID:[ \t]*(-?\d+)/i);
12512
+ if (!file_id_match)
12513
+ return null;
12514
+ const guid_match = ref_text.match(/guid:[ \t]*([a-f0-9]{32})/i);
12515
+ const type_match = ref_text.match(/type:[ \t]*(-?\d+)/i);
12516
+ const parsed = { file_id: file_id_match[1] };
12517
+ if (guid_match)
12518
+ parsed.guid = guid_match[1].toLowerCase();
12519
+ if (type_match)
12520
+ parsed.type = parseInt(type_match[1], 10);
12521
+ return parsed;
12522
+ }
12523
+ function extract_prefab_list_section(raw, key) {
12524
+ const lines = raw.split(`
12525
+ `);
12526
+ const key_pattern = new RegExp(`^(\\s*)${key}:[ \\t]*(.*)$`);
12527
+ let key_index = -1;
12528
+ let key_indent = 0;
12529
+ for (let i = 0;i < lines.length; i++) {
12530
+ const match = lines[i].match(key_pattern);
12531
+ if (match) {
12532
+ key_index = i;
12533
+ key_indent = match[1].length;
12534
+ break;
12535
+ }
12536
+ }
12537
+ if (key_index === -1)
12538
+ return null;
12539
+ const section_lines = [lines[key_index]];
12540
+ for (let i = key_index + 1;i < lines.length; i++) {
12541
+ const line = lines[i];
12542
+ const trimmed = line.trim();
12543
+ if (trimmed.length === 0) {
12544
+ section_lines.push(line);
12545
+ continue;
12546
+ }
12547
+ const indent = (line.match(/^\s*/) || [""])[0].length;
12548
+ if (indent < key_indent)
12549
+ break;
12550
+ if (indent === key_indent && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
12551
+ break;
12552
+ }
12553
+ section_lines.push(line);
12554
+ }
12555
+ return { key_indent, section_lines };
12556
+ }
12557
+ function parse_removed_list(raw, key) {
12558
+ const section = extract_prefab_list_section(raw, key);
12559
+ if (!section)
12560
+ return [];
12561
+ const key_line = section.section_lines[0].trim();
12562
+ if (key_line.endsWith("[]"))
12563
+ return [];
12564
+ const refs = section.section_lines.join(`
12565
+ `).match(/\{[^}]*fileID:[^}]*\}/g) || [];
12566
+ const dedup = new Map;
12567
+ for (const ref of refs) {
12568
+ const parsed = parse_unity_ref(ref);
12569
+ if (!parsed)
12570
+ continue;
12571
+ const key_text = `${parsed.file_id}|${parsed.guid ?? ""}|${parsed.type ?? ""}`;
12572
+ if (!dedup.has(key_text))
12573
+ dedup.set(key_text, parsed);
12574
+ }
12575
+ return [...dedup.values()];
12576
+ }
12577
+ function parse_added_list(raw, key) {
12578
+ const section = extract_prefab_list_section(raw, key);
12579
+ if (!section)
12580
+ return [];
12581
+ const key_line = section.section_lines[0].trim();
12582
+ if (key_line.endsWith("[]"))
12583
+ return [];
12584
+ const lines = section.section_lines.slice(1);
12585
+ const entries = [];
12586
+ let i = 0;
12587
+ while (i < lines.length) {
12588
+ const line = lines[i];
12589
+ const trimmed = line.trimStart();
12590
+ if (!trimmed.startsWith("-")) {
12591
+ i++;
12592
+ continue;
12593
+ }
12594
+ const entry_indent = (line.match(/^\s*/) || [""])[0].length;
12595
+ const entry_lines = [line];
12596
+ i++;
12597
+ while (i < lines.length) {
12598
+ const next = lines[i];
12599
+ const next_trimmed = next.trim();
12600
+ const next_indent = (next.match(/^\s*/) || [""])[0].length;
12601
+ if (next_trimmed.length > 0 && next_indent === entry_indent && next_trimmed.startsWith("-")) {
12602
+ break;
12603
+ }
12604
+ entry_lines.push(next);
12605
+ i++;
12606
+ }
12607
+ const entry_text = entry_lines.join(`
12608
+ `);
12609
+ const target_match = entry_text.match(/targetCorrespondingSourceObject:[ \t]*(\{[^}]+\})/);
12610
+ const insert_match = entry_text.match(/insertIndex:[ \t]*(-?\d+)/);
12611
+ const added_match = entry_text.match(/addedObject:[ \t]*(\{[^}]+\})/);
12612
+ entries.push({
12613
+ target_corresponding_source_object: target_match ? parse_unity_ref(target_match[1]) : null,
12614
+ insert_index: insert_match ? parseInt(insert_match[1], 10) : null,
12615
+ added_object: added_match ? parse_unity_ref(added_match[1]) : null
12616
+ });
12617
+ }
12618
+ return entries;
12619
+ }
11035
12620
  function parse_material_yaml(content) {
11036
12621
  const lines = content.replace(/\r/g, "").split(`
11037
12622
  `);
@@ -11230,7 +12815,7 @@ function parse_material_yaml(content) {
11230
12815
  return result;
11231
12816
  }
11232
12817
  function categorize_asset(filePath) {
11233
- const ext = import_path8.extname(filePath).toLowerCase();
12818
+ const ext = import_path11.extname(filePath).toLowerCase();
11234
12819
  const categories = {
11235
12820
  ".cs": "script",
11236
12821
  ".mat": "material",
@@ -11274,12 +12859,12 @@ function categorize_asset(filePath) {
11274
12859
  return categories[ext] || "other";
11275
12860
  }
11276
12861
  function find_project_root_from_file(filePath) {
11277
- let dir = import_path8.dirname(import_path8.resolve(filePath));
12862
+ let dir = import_path11.dirname(import_path11.resolve(filePath));
11278
12863
  for (let i = 0;i < 20; i++) {
11279
- if (import_fs18.existsSync(import_path8.join(dir, "Assets")) && import_fs18.existsSync(import_path8.join(dir, "ProjectSettings"))) {
12864
+ if (import_fs19.existsSync(import_path11.join(dir, "Assets")) && import_fs19.existsSync(import_path11.join(dir, "ProjectSettings"))) {
11280
12865
  return dir;
11281
12866
  }
11282
- const parent = import_path8.dirname(dir);
12867
+ const parent = import_path11.dirname(dir);
11283
12868
  if (parent === dir)
11284
12869
  break;
11285
12870
  dir = parent;
@@ -11289,13 +12874,13 @@ function find_project_root_from_file(filePath) {
11289
12874
  function get_editor_log_path() {
11290
12875
  const p = import_os.platform();
11291
12876
  if (p === "darwin")
11292
- return import_path8.join(import_os.homedir(), "Library", "Logs", "Unity", "Editor.log");
12877
+ return import_path11.join(import_os.homedir(), "Library", "Logs", "Unity", "Editor.log");
11293
12878
  if (p === "win32") {
11294
- const localAppData = process.env.LOCALAPPDATA || import_path8.join(import_os.homedir(), "AppData", "Local");
11295
- return import_path8.join(localAppData, "Unity", "Editor", "Editor.log");
12879
+ const localAppData = process.env.LOCALAPPDATA || import_path11.join(import_os.homedir(), "AppData", "Local");
12880
+ return import_path11.join(localAppData, "Unity", "Editor", "Editor.log");
11296
12881
  }
11297
12882
  if (p === "linux")
11298
- return import_path8.join(import_os.homedir(), ".config", "unity3d", "Editor.log");
12883
+ return import_path11.join(import_os.homedir(), ".config", "unity3d", "Editor.log");
11299
12884
  return null;
11300
12885
  }
11301
12886
  function parse_log_line_timestamp(line) {
@@ -11626,19 +13211,19 @@ function walk_files(dir, extensions) {
11626
13211
  const current = stack.pop();
11627
13212
  let entries;
11628
13213
  try {
11629
- entries = import_fs18.readdirSync(current);
13214
+ entries = import_fs19.readdirSync(current);
11630
13215
  } catch {
11631
13216
  continue;
11632
13217
  }
11633
13218
  for (const entry of entries) {
11634
13219
  if (entry.startsWith("."))
11635
13220
  continue;
11636
- const full = import_path8.join(current, entry);
13221
+ const full = import_path11.join(current, entry);
11637
13222
  try {
11638
- const stat = import_fs18.statSync(full);
13223
+ const stat = import_fs19.statSync(full);
11639
13224
  if (stat.isDirectory()) {
11640
13225
  stack.push(full);
11641
- } else if (extensions.has(import_path8.extname(full).toLowerCase())) {
13226
+ } else if (extensions.has(import_path11.extname(full).toLowerCase())) {
11642
13227
  results.push(full);
11643
13228
  }
11644
13229
  } catch {
@@ -11649,7 +13234,7 @@ function walk_files(dir, extensions) {
11649
13234
  return results;
11650
13235
  }
11651
13236
  function validate_unity_yaml(file) {
11652
- if (!import_fs18.existsSync(file)) {
13237
+ if (!import_fs19.existsSync(file)) {
11653
13238
  return `File not found: ${file}`;
11654
13239
  }
11655
13240
  try {
@@ -11735,10 +13320,17 @@ function build_read_command(getScanner) {
11735
13320
  }
11736
13321
  if (result.total === 0 && !result.error) {
11737
13322
  const prefabInstances = result.prefabInstances;
13323
+ let variantDetected = false;
13324
+ let variantResolvedFromSource = false;
11738
13325
  if (prefabInstances && prefabInstances.length > 0) {
13326
+ variantDetected = true;
11739
13327
  try {
11740
13328
  const doc = UnityDocument.from_file(file);
11741
- const projectPath = find_unity_project_root(import_path8.dirname(file));
13329
+ const strippedGoCount = doc.blocks.filter((b) => b.class_id === 1 && b.is_stripped).length;
13330
+ if (strippedGoCount > 0) {
13331
+ variantDetected = true;
13332
+ }
13333
+ const projectPath = find_unity_project_root(import_path11.dirname(file));
11742
13334
  const resolved = resolve_source_prefab(doc, file, projectPath ?? undefined);
11743
13335
  if (resolved) {
11744
13336
  const sourceResult = getScanner().inspect_all_paginated({
@@ -11772,6 +13364,7 @@ function build_read_command(getScanner) {
11772
13364
  resultRecord.resolvedFromSource = true;
11773
13365
  resultRecord.sourcePrefab = resolved.source_path;
11774
13366
  resultRecord.sourceGuid = resolved.source_guid;
13367
+ variantResolvedFromSource = true;
11775
13368
  const variantNote = `PrefabVariant resolved from source prefab: ${resolved.source_path}`;
11776
13369
  result.warning = result.warning ? `${result.warning}; ${variantNote}` : variantNote;
11777
13370
  }
@@ -11780,10 +13373,15 @@ function build_read_command(getScanner) {
11780
13373
  }
11781
13374
  if (result.total === 0) {
11782
13375
  try {
11783
- const fileSize = import_fs18.statSync(file).size;
13376
+ const fileSize = import_fs19.statSync(file).size;
11784
13377
  if (fileSize > 100) {
11785
- const corruptWarning = "File has valid Unity YAML header but contains no parseable GameObjects -- file may be corrupt or malformed";
11786
- result.warning = result.warning ? `${result.warning}; ${corruptWarning}` : corruptWarning;
13378
+ if (variantDetected && !variantResolvedFromSource) {
13379
+ const variantWarning = "PrefabVariant detected but source prefab hierarchy could not be resolved (missing project context or GUID cache). Use --project or run setup to resolve source hierarchy.";
13380
+ result.warning = result.warning ? `${result.warning}; ${variantWarning}` : variantWarning;
13381
+ } else {
13382
+ const corruptWarning = "File has valid Unity YAML header but contains no parseable GameObjects -- file may be corrupt or malformed";
13383
+ result.warning = result.warning ? `${result.warning}; ${corruptWarning}` : corruptWarning;
13384
+ }
11787
13385
  }
11788
13386
  } catch {}
11789
13387
  }
@@ -11893,7 +13491,7 @@ function build_read_command(getScanner) {
11893
13491
  console.log(JSON.stringify({ error: matValidationError }, null, 2));
11894
13492
  process.exit(1);
11895
13493
  }
11896
- const content = import_fs18.readFileSync(file, "utf-8");
13494
+ const content = import_fs19.readFileSync(file, "utf-8");
11897
13495
  if (!content.includes("Material:")) {
11898
13496
  console.log(JSON.stringify({ error: `File "${file}" does not contain a Material block. Use 'read asset' for generic Unity YAML files.` }, null, 2));
11899
13497
  process.exit(1);
@@ -11935,13 +13533,13 @@ function build_read_command(getScanner) {
11935
13533
  }, null, 2));
11936
13534
  });
11937
13535
  cmd.command("dependencies <file>").description("List asset dependencies (GUIDs referenced by this file)").option("--project <path>", "Unity project root (for GUID resolution)").option("--unresolved", "Show only GUIDs that could not be resolved").option("--recursive [depth]", "Follow dependency chain N levels deep (default: 3)").option("-j, --json", "Output as JSON").action((file, options) => {
11938
- if (!import_fs18.existsSync(file)) {
13536
+ if (!import_fs19.existsSync(file)) {
11939
13537
  console.log(JSON.stringify({ error: `File not found: ${file}` }, null, 2));
11940
13538
  process.exit(1);
11941
13539
  }
11942
13540
  let content;
11943
13541
  try {
11944
- content = import_fs18.readFileSync(file, "utf-8");
13542
+ content = import_fs19.readFileSync(file, "utf-8");
11945
13543
  } catch {
11946
13544
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }, null, 2));
11947
13545
  process.exit(1);
@@ -11982,7 +13580,7 @@ function build_read_command(getScanner) {
11982
13580
  const result = [];
11983
13581
  let fileContent;
11984
13582
  try {
11985
- fileContent = import_fs18.readFileSync(filePath, "utf-8");
13583
+ fileContent = import_fs19.readFileSync(filePath, "utf-8");
11986
13584
  } catch {
11987
13585
  return [];
11988
13586
  }
@@ -11999,7 +13597,7 @@ function build_read_command(getScanner) {
11999
13597
  const rAbsPath = cache.resolve_absolute(g);
12000
13598
  const rType = rPath ? categorize_asset(rPath) : "unknown";
12001
13599
  const dep = { guid: g, path: rPath, type: rType, depth };
12002
- if (rAbsPath && depth < max_depth && import_fs18.existsSync(rAbsPath)) {
13600
+ if (rAbsPath && depth < max_depth && import_fs19.existsSync(rAbsPath)) {
12003
13601
  const subs = traverse_file(rAbsPath, depth + 1);
12004
13602
  if (subs.length > 0)
12005
13603
  dep.sub_dependencies = subs;
@@ -12028,7 +13626,7 @@ function build_read_command(getScanner) {
12028
13626
  const rDep = { guid: dep.guid, path: dep.path, type: dep.type, depth: 0 };
12029
13627
  if (dep.path) {
12030
13628
  const absPath = cache.resolve_absolute(dep.guid);
12031
- if (absPath && import_fs18.existsSync(absPath)) {
13629
+ if (absPath && import_fs19.existsSync(absPath)) {
12032
13630
  const subs = traverse_file(absPath, 1);
12033
13631
  if (subs.length > 0)
12034
13632
  rDep.sub_dependencies = subs;
@@ -12056,38 +13654,41 @@ function build_read_command(getScanner) {
12056
13654
  by_type: options.unresolved ? undefined : byType
12057
13655
  };
12058
13656
  if (!cache) {
12059
- output._hint = "Run 'setup <project>' to resolve GUID paths";
13657
+ output._hint = "Run 'setup' (or 'setup -p <path>') to resolve GUID paths";
12060
13658
  }
12061
13659
  console.log(JSON.stringify(output, null, 2));
12062
13660
  });
12063
- cmd.command("settings <project_path>").description("Read Unity project settings (TagManager, DynamicsManager, QualitySettings, TimeManager, etc.)").option("-s, --setting <name>", "Setting name or alias (tags, physics, quality, time)", "TagManager").option("-j, --json", "Output as JSON").action((project_path, options) => {
13661
+ cmd.command("settings").description("Read Unity project settings (TagManager, DynamicsManager, QualitySettings, TimeManager, etc.)").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-s, --setting <name>", "Setting name or alias (tags, physics, quality, time)", "TagManager").option("-j, --json", "Output as JSON").action((options) => {
13662
+ const resolvedProjectPath = resolve_project_path(options.project);
12064
13663
  const result = read_settings({
12065
- project_path,
13664
+ project_path: resolvedProjectPath,
12066
13665
  setting: options.setting
12067
13666
  });
12068
13667
  console.log(JSON.stringify(result, null, 2));
12069
13668
  if (!result.success)
12070
13669
  process.exit(1);
12071
13670
  });
12072
- cmd.command("build <project_path>").description("Read build settings (scene list, build profiles)").option("-j, --json", "Output as JSON").action((project_path, _options) => {
13671
+ cmd.command("build").description("Read build settings (scene list, build profiles)").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((options) => {
13672
+ const resolvedProjectPath = resolve_project_path(options.project);
12073
13673
  try {
12074
- const result = get_build_settings(project_path);
13674
+ const result = get_build_settings(resolvedProjectPath);
12075
13675
  console.log(JSON.stringify(result, null, 2));
12076
13676
  } catch (err) {
12077
13677
  console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
12078
13678
  process.exitCode = 1;
12079
13679
  }
12080
13680
  });
12081
- cmd.command("scenes <project_path>").description('Read build scenes (alias for "read build")').option("-j, --json", "Output as JSON").action((project_path, _options) => {
13681
+ cmd.command("scenes").description('Read build scenes (alias for "read build")').option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((options) => {
13682
+ const resolvedProjectPath = resolve_project_path(options.project);
12082
13683
  try {
12083
- const result = get_build_settings(project_path);
13684
+ const result = get_build_settings(resolvedProjectPath);
12084
13685
  console.log(JSON.stringify(result, null, 2));
12085
13686
  } catch (err) {
12086
13687
  console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
12087
13688
  process.exitCode = 1;
12088
13689
  }
12089
13690
  });
12090
- cmd.command("overrides <file> <prefab_instance>").description("Read PrefabInstance override modifications").option("--flat", "Output simplified list").option("-j, --json", "Output as JSON").action((file, prefab_instance, options) => {
13691
+ cmd.command("overrides <file> <prefab_instance>").description("Read PrefabInstance overrides (modifications + removed/added state)").option("--flat", "Output simplified list").option("-j, --json", "Output as JSON").action((file, prefab_instance, options) => {
12091
13692
  try {
12092
13693
  const doc = UnityDocument.from_file(file);
12093
13694
  let block = null;
@@ -12150,6 +13751,10 @@ function build_read_command(getScanner) {
12150
13751
  i++;
12151
13752
  }
12152
13753
  }
13754
+ const removed_components = parse_removed_list(block.raw, "m_RemovedComponents");
13755
+ const removed_gameobjects = parse_removed_list(block.raw, "m_RemovedGameObjects");
13756
+ const added_gameobjects = parse_added_list(block.raw, "m_AddedGameObjects");
13757
+ const added_components = parse_added_list(block.raw, "m_AddedComponents");
12153
13758
  if (options.flat) {
12154
13759
  const flat = modifications.map((m) => ({
12155
13760
  property_path: m.property_path,
@@ -12158,7 +13763,14 @@ function build_read_command(getScanner) {
12158
13763
  }));
12159
13764
  console.log(JSON.stringify(flat, null, 2));
12160
13765
  } else {
12161
- console.log(JSON.stringify(modifications, null, 2));
13766
+ console.log(JSON.stringify({
13767
+ prefab_instance_id: block.file_id,
13768
+ modifications,
13769
+ removed_components,
13770
+ removed_gameobjects,
13771
+ added_gameobjects,
13772
+ added_components
13773
+ }, null, 2));
12162
13774
  }
12163
13775
  } catch (err) {
12164
13776
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2));
@@ -12213,7 +13825,7 @@ function build_read_command(getScanner) {
12213
13825
  });
12214
13826
  cmd.command("target <file> <gameobject_name> [component_type]").description("Build a --target reference string for prefab override commands").option("-p, --project <path>", "Unity project path").option("-j, --json", "Output as JSON").action((file, gameobject_name, component_type, options) => {
12215
13827
  try {
12216
- if (!import_fs18.existsSync(file)) {
13828
+ if (!import_fs19.existsSync(file)) {
12217
13829
  console.log(JSON.stringify({ error: `File not found: ${file}` }, null, 2));
12218
13830
  process.exit(1);
12219
13831
  }
@@ -12274,12 +13886,12 @@ function build_read_command(getScanner) {
12274
13886
  if (classId === null && compBlock.class_id === 114) {
12275
13887
  const scriptMatch = compBlock.raw.match(/m_Script:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/);
12276
13888
  if (scriptMatch) {
12277
- const project = options.project || find_unity_project_root(import_path8.dirname(file));
13889
+ const project = options.project || find_unity_project_root(import_path11.dirname(file));
12278
13890
  if (project) {
12279
13891
  const cache = load_guid_cache(project);
12280
13892
  if (cache) {
12281
13893
  const scriptPath = cache.resolve(scriptMatch[1]);
12282
- if (scriptPath && import_path8.basename(scriptPath, ".cs").toLowerCase() === component_type.toLowerCase().replace(/\.cs$/, "")) {
13894
+ if (scriptPath && import_path11.basename(scriptPath, ".cs").toLowerCase() === component_type.toLowerCase().replace(/\.cs$/, "")) {
12283
13895
  targetFileId = refId;
12284
13896
  compFound = true;
12285
13897
  break;
@@ -12294,7 +13906,7 @@ function build_read_command(getScanner) {
12294
13906
  process.exit(1);
12295
13907
  }
12296
13908
  } else {
12297
- const project = options.project || find_unity_project_root(import_path8.dirname(file));
13909
+ const project = options.project || find_unity_project_root(import_path11.dirname(file));
12298
13910
  const resolved = resolve_source_prefab(doc, file, project ?? undefined);
12299
13911
  if (resolved) {
12300
13912
  const sourceDoc = UnityDocument.from_file(resolved.source_path);
@@ -12333,7 +13945,7 @@ function build_read_command(getScanner) {
12333
13945
  }
12334
13946
  });
12335
13947
  cmd.command("script <file>").description("Extract C# type declarations from a .cs file or .NET DLL").option("-j, --json", "Output as JSON").action((file, _options) => {
12336
- if (!import_fs18.existsSync(file)) {
13948
+ if (!import_fs19.existsSync(file)) {
12337
13949
  console.log(JSON.stringify({ error: `File not found: ${file}` }, null, 2));
12338
13950
  process.exit(1);
12339
13951
  }
@@ -12369,7 +13981,7 @@ function build_read_command(getScanner) {
12369
13981
  console.log(JSON.stringify({ error: "Native module not available" }, null, 2));
12370
13982
  process.exit(1);
12371
13983
  }
12372
- const projectPath = import_path8.resolve(options.project);
13984
+ const projectPath = import_path11.resolve(options.project);
12373
13985
  const validSources = ["assets", "packages", "dlls", "all"];
12374
13986
  if (!validSources.includes(options.source)) {
12375
13987
  console.log(JSON.stringify({ error: `Invalid --source "${options.source}". Valid values: ${validSources.join(", ")}` }, null, 2));
@@ -12418,20 +14030,18 @@ function build_read_command(getScanner) {
12418
14030
  types: displayed
12419
14031
  }, null, 2));
12420
14032
  });
12421
- cmd.command("log [project-path]").description("Read and filter the Unity Editor.log").option("--path <file>", "Path to Editor.log (auto-detected if omitted)").option("--project <path>", "Filter to log entries from a specific Unity project session").option("--tail <n>", "Show last N lines (default 50)", "50").option("--errors", "Show only error entries").option("--warnings", "Show only warning entries").option("--compile-errors", "Show only C# compilation errors").option("--import-errors", "Show only asset import errors").option("--since <timestamp>", "Filter entries after this timestamp (YYYY-MM-DD or HH:MM:SS)").option("--search <pattern>", "Regex filter on log content").option("-j, --json", "Output as JSON").action((projectPath, options) => {
12422
- if (projectPath && !options.project)
12423
- options.project = projectPath;
14033
+ cmd.command("log").description("Read and filter the Unity Editor.log").option("--path <file>", "Path to Editor.log (auto-detected if omitted)").option("--project <path>", "Filter to log entries from a specific Unity project session").option("--tail <n>", "Show last N lines (default 50)", "50").option("--errors", "Show only error entries").option("--warnings", "Show only warning entries").option("--compile-errors", "Show only C# compilation errors").option("--import-errors", "Show only asset import errors").option("--since <timestamp>", "Filter entries after this timestamp (YYYY-MM-DD or HH:MM:SS)").option("--search <pattern>", "Regex filter on log content").option("-j, --json", "Output as JSON").action((options) => {
12424
14034
  const logPath = options.path || get_editor_log_path();
12425
- if (!logPath || !import_fs18.existsSync(logPath)) {
14035
+ if (!logPath || !import_fs19.existsSync(logPath)) {
12426
14036
  console.log(JSON.stringify({ error: `Editor.log not found${logPath ? `: ${logPath}` : ". Could not detect platform log path."}` }, null, 2));
12427
14037
  process.exit(1);
12428
14038
  }
12429
- const content = import_fs18.readFileSync(logPath, "utf-8");
14039
+ const content = import_fs19.readFileSync(logPath, "utf-8");
12430
14040
  let lines = content.split(/\r?\n/);
12431
14041
  if (options.project) {
12432
- const projectPath2 = import_path8.resolve(options.project);
12433
- const projectName = import_path8.basename(projectPath2);
12434
- const escapedPath = projectPath2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14042
+ const projectPath = import_path11.resolve(options.project);
14043
+ const projectName = import_path11.basename(projectPath);
14044
+ const escapedPath = projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12435
14045
  const escapedName = projectName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12436
14046
  const session_markers = [
12437
14047
  new RegExp(`Loading project at '${escapedPath}'`),
@@ -12551,11 +14161,11 @@ function build_read_command(getScanner) {
12551
14161
  });
12552
14162
  cmd.command("meta <file>").description("Read a Unity .meta file and show importer settings").option("--summary", "Show importer type and key settings only").option("-j, --json", "Output as JSON").action((file, options) => {
12553
14163
  const metaPath = file.endsWith(".meta") ? file : `${file}.meta`;
12554
- if (!import_fs18.existsSync(metaPath)) {
14164
+ if (!import_fs19.existsSync(metaPath)) {
12555
14165
  console.log(JSON.stringify({ error: `Meta file not found: ${metaPath}` }, null, 2));
12556
14166
  process.exit(1);
12557
14167
  }
12558
- const content = import_fs18.readFileSync(metaPath, "utf-8");
14168
+ const content = import_fs19.readFileSync(metaPath, "utf-8");
12559
14169
  const lines = content.split(/\r?\n/);
12560
14170
  let guid = "";
12561
14171
  let importer_type = "Unknown";
@@ -12643,7 +14253,7 @@ function build_read_command(getScanner) {
12643
14253
  console.log(JSON.stringify({ error: animValidationError }, null, 2));
12644
14254
  process.exit(1);
12645
14255
  }
12646
- const content = import_fs18.readFileSync(file, "utf-8");
14256
+ const content = import_fs19.readFileSync(file, "utf-8");
12647
14257
  const clip = parse_animation_yaml(content, options.curves === true);
12648
14258
  if (!clip) {
12649
14259
  console.log(JSON.stringify({ error: `No AnimationClip found in "${file}". Is this an .anim file?` }, null, 2));
@@ -12704,7 +14314,7 @@ function build_read_command(getScanner) {
12704
14314
  console.log(JSON.stringify({ error: ctrlValidationError }, null, 2));
12705
14315
  process.exit(1);
12706
14316
  }
12707
- const content = import_fs18.readFileSync(file, "utf-8");
14317
+ const content = import_fs19.readFileSync(file, "utf-8");
12708
14318
  const blocks = split_yaml_blocks(content);
12709
14319
  const controller_block = blocks.find((b) => b.class_id === 91);
12710
14320
  if (!controller_block) {
@@ -12876,7 +14486,7 @@ function build_read_command(getScanner) {
12876
14486
  }
12877
14487
  const states_out = { file, states_by_layer: by_layer };
12878
14488
  if (needs_setup_hint)
12879
- states_out._hint = "Run 'setup <project>' to resolve motion paths";
14489
+ states_out._hint = "Run 'setup' (or 'setup -p <path>') to resolve motion paths";
12880
14490
  console.log(JSON.stringify(states_out, null, 2));
12881
14491
  return;
12882
14492
  }
@@ -12895,7 +14505,7 @@ function build_read_command(getScanner) {
12895
14505
  if (options.summary) {
12896
14506
  console.log(JSON.stringify({
12897
14507
  file,
12898
- name: yaml_field(controller_block.raw, "m_Name") || import_path8.basename(file),
14508
+ name: yaml_field(controller_block.raw, "m_Name") || import_path11.basename(file),
12899
14509
  parameter_count: params.length,
12900
14510
  layer_count: layers.length,
12901
14511
  state_count: states.length,
@@ -12908,7 +14518,7 @@ function build_read_command(getScanner) {
12908
14518
  default_state_names[s.file_id] = s.name;
12909
14519
  const default_out = {
12910
14520
  file,
12911
- name: yaml_field(controller_block.raw, "m_Name") || import_path8.basename(file),
14521
+ name: yaml_field(controller_block.raw, "m_Name") || import_path11.basename(file),
12912
14522
  parameters: params,
12913
14523
  layers: layers.map((l) => l.name),
12914
14524
  states: states.map((s) => ({
@@ -12925,16 +14535,17 @@ function build_read_command(getScanner) {
12925
14535
  }))
12926
14536
  };
12927
14537
  if (needs_setup_hint)
12928
- default_out._hint = "Run 'setup <project>' to resolve motion paths";
14538
+ default_out._hint = "Run 'setup' (or 'setup -p <path>') to resolve motion paths";
12929
14539
  console.log(JSON.stringify(default_out, null, 2));
12930
14540
  });
12931
- cmd.command("dependents <project_path> <guid>").description("Find which files reference a given GUID (reverse dependency lookup)").option("--type <type>", "Filter to specific file types (scene, prefab, mat, etc.)").option("-j, --json", "Output as JSON").action((project_path, guid, options) => {
14541
+ cmd.command("dependents <guid>").description("Find which files reference a given GUID (reverse dependency lookup)").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--type <type>", "Filter to specific file types (scene, prefab, mat, etc.)").option("-j, --json", "Output as JSON").action((guid, options) => {
12932
14542
  if (!/^[0-9a-f]{32}$/i.test(guid)) {
12933
14543
  console.log(JSON.stringify({ error: "GUID must be a 32-character hexadecimal string" }, null, 2));
12934
14544
  process.exit(1);
12935
14545
  }
12936
- const assetsDir = import_path8.join(import_path8.resolve(project_path), "Assets");
12937
- if (!import_fs18.existsSync(assetsDir)) {
14546
+ const project_path = resolve_project_path(options.project);
14547
+ const assetsDir = import_path11.join(import_path11.resolve(project_path), "Assets");
14548
+ if (!import_fs19.existsSync(assetsDir)) {
12938
14549
  console.log(JSON.stringify({ error: `Assets directory not found in "${project_path}"` }, null, 2));
12939
14550
  process.exit(1);
12940
14551
  }
@@ -12963,9 +14574,9 @@ function build_read_command(getScanner) {
12963
14574
  const guid_pattern = `guid: ${guid}`;
12964
14575
  for (const f of files) {
12965
14576
  try {
12966
- const fc = import_fs18.readFileSync(f, "utf-8");
14577
+ const fc = import_fs19.readFileSync(f, "utf-8");
12967
14578
  if (fc.includes(guid_pattern)) {
12968
- const rel = import_path8.relative(import_path8.resolve(project_path), f);
14579
+ const rel = import_path11.relative(import_path11.resolve(project_path), f);
12969
14580
  const ftype = categorize_asset(f);
12970
14581
  referencing.push({ path: rel, type: ftype });
12971
14582
  }
@@ -12984,17 +14595,17 @@ function build_read_command(getScanner) {
12984
14595
  by_type[r.type].push(r.path);
12985
14596
  }
12986
14597
  console.log(JSON.stringify({
12987
- project_path: import_path8.resolve(project_path),
14598
+ project_path: import_path11.resolve(project_path),
12988
14599
  guid,
12989
14600
  total_references: filtered.length,
12990
14601
  by_type
12991
14602
  }, null, 2));
12992
14603
  });
12993
- cmd.command("unused <project_path>").description("Find potentially unused assets (zero inbound GUID references)").option("--type <type>", "Filter to specific asset types").option("--ignore <glob>", "Exclude paths matching this pattern").option("--max <n>", "Maximum results to return (default 200)", "200").option("-j, --json", "Output as JSON").action((project_path, options) => {
12994
- const resolvedProject = import_path8.resolve(project_path);
12995
- const assetsDir = import_path8.join(resolvedProject, "Assets");
12996
- if (!import_fs18.existsSync(assetsDir)) {
12997
- console.log(JSON.stringify({ error: `Assets directory not found in "${project_path}"` }, null, 2));
14604
+ cmd.command("unused").description("Find potentially unused assets (zero inbound GUID references)").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--type <type>", "Filter to specific asset types").option("--ignore <glob>", "Exclude paths matching this pattern").option("--max <n>", "Maximum results to return (default 200)", "200").option("-j, --json", "Output as JSON").action((options) => {
14605
+ const resolvedProject = resolve_project_path(options.project);
14606
+ const assetsDir = import_path11.join(resolvedProject, "Assets");
14607
+ if (!import_fs19.existsSync(assetsDir)) {
14608
+ console.log(JSON.stringify({ error: `Assets directory not found in "${resolvedProject}"` }, null, 2));
12998
14609
  process.exit(1);
12999
14610
  }
13000
14611
  const guidCacheObj = load_guid_cache(resolvedProject);
@@ -13021,7 +14632,7 @@ function build_read_command(getScanner) {
13021
14632
  const guid_re = /guid:\s*([a-f0-9]{32})/g;
13022
14633
  for (const f of files) {
13023
14634
  try {
13024
- const fc = import_fs18.readFileSync(f, "utf-8");
14635
+ const fc = import_fs19.readFileSync(f, "utf-8");
13025
14636
  let gm;
13026
14637
  while ((gm = guid_re.exec(fc)) !== null) {
13027
14638
  referenced_guids.add(gm[1]);
@@ -13030,10 +14641,10 @@ function build_read_command(getScanner) {
13030
14641
  continue;
13031
14642
  }
13032
14643
  }
13033
- const buildSettingsPath = import_path8.join(resolvedProject, "ProjectSettings", "EditorBuildSettings.asset");
14644
+ const buildSettingsPath = import_path11.join(resolvedProject, "ProjectSettings", "EditorBuildSettings.asset");
13034
14645
  const build_scene_guids = new Set;
13035
- if (import_fs18.existsSync(buildSettingsPath)) {
13036
- const bsc = import_fs18.readFileSync(buildSettingsPath, "utf-8");
14646
+ if (import_fs19.existsSync(buildSettingsPath)) {
14647
+ const bsc = import_fs19.readFileSync(buildSettingsPath, "utf-8");
13037
14648
  let bm;
13038
14649
  while ((bm = guid_re.exec(bsc)) !== null) {
13039
14650
  build_scene_guids.add(bm[1]);
@@ -13054,7 +14665,7 @@ function build_read_command(getScanner) {
13054
14665
  continue;
13055
14666
  if (ignore_pattern && ignore_pattern.test(asset_path))
13056
14667
  continue;
13057
- if (import_path8.extname(asset_path) === "")
14668
+ if (import_path11.extname(asset_path) === "")
13058
14669
  continue;
13059
14670
  const asset_type = categorize_asset(asset_path);
13060
14671
  if (options.type && asset_type !== options.type)
@@ -13085,7 +14696,8 @@ function build_read_command(getScanner) {
13085
14696
  by_type
13086
14697
  }, null, 2));
13087
14698
  });
13088
- cmd.command("manifest <project_path>").description("List packages from Packages/manifest.json").option("--search <pattern>", "Filter packages by name pattern").option("-j, --json", "Output as JSON").action((project_path, options) => {
14699
+ cmd.command("manifest").description("List packages from Packages/manifest.json").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--search <pattern>", "Filter packages by name pattern").option("-j, --json", "Output as JSON").action((options) => {
14700
+ const project_path = resolve_project_path(options.project);
13089
14701
  const result = list_packages(project_path, options.search);
13090
14702
  if ("error" in result) {
13091
14703
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
@@ -13155,8 +14767,8 @@ function build_read_command(getScanner) {
13155
14767
  }
13156
14768
 
13157
14769
  // src/cmd-update.ts
13158
- var import_fs19 = require("fs");
13159
- var import_path9 = require("path");
14770
+ var import_fs20 = require("fs");
14771
+ var import_path12 = require("path");
13160
14772
 
13161
14773
  // src/animator-utils.ts
13162
14774
  function split_yaml_blocks2(content) {
@@ -13285,7 +14897,13 @@ function parseVector(str) {
13285
14897
  }
13286
14898
  return { x: parts[0], y: parts[1], z: parts[2] };
13287
14899
  }
13288
- function resolve_transform_id(scanner, file, identifier) {
14900
+ function resolve_transform_id(scanner, file, identifier, by_id = false) {
14901
+ if (by_id) {
14902
+ if (!/^-?\d+$/.test(identifier)) {
14903
+ return { error: `Invalid fileID: "${identifier}" \u2014 expected a numeric value when using --by-id` };
14904
+ }
14905
+ return { transform_id: identifier };
14906
+ }
13289
14907
  let resolved_id = identifier;
13290
14908
  if (!/^-?\d+$/.test(identifier)) {
13291
14909
  const matches = scanner.find_by_name(file, identifier, false);
@@ -13310,7 +14928,17 @@ function resolve_transform_id(scanner, file, identifier) {
13310
14928
  }
13311
14929
  function build_update_command(getScanner) {
13312
14930
  const cmd = new Command("update").description("Update Unity object properties, transforms, settings, and hierarchy");
13313
- cmd.command("gameobject <file> <object_name> <property> <value>").description("Edit GameObject property value safely").option("-j, --json", "Output as JSON").option("-p, --project <path>", "Unity project path (for tag validation)").action((file, object_name, property, value, options) => {
14931
+ async function check_loaded_edit_protection(file, bypass, project_path) {
14932
+ const guard = await enforce_loaded_edit_protection(file, bypass, project_path);
14933
+ if (guard.allowed)
14934
+ return true;
14935
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
14936
+ process.exitCode = 1;
14937
+ return false;
14938
+ }
14939
+ cmd.command("gameobject <file> <object_name> <property> <value>").description("Edit GameObject property value safely").option("-j, --json", "Output as JSON").option("-p, --project <path>", "Unity project path (for tag validation)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, object_name, property, value, options) => {
14940
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection, options.project))
14941
+ return;
13314
14942
  const result = editProperty({
13315
14943
  file_path: file,
13316
14944
  object_name,
@@ -13322,7 +14950,7 @@ function build_update_command(getScanner) {
13322
14950
  if (!result.success)
13323
14951
  process.exitCode = 1;
13324
14952
  });
13325
- cmd.command("component <file> <file_id> <property> <value>").description("Edit any component property by file ID. Supports dotted paths (m_LocalPosition.x) and array paths (m_Materials.Array.data[0]). Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("-j, --json", "Output as JSON").option("-p, --project <path>", "Unity project path (for asset reference resolution)").action((file, file_id, property, value, options) => {
14953
+ cmd.command("component <file> <file_id> <property> <value>").description("Edit any component property by file ID. Supports dotted paths (m_LocalPosition.x) and array paths (m_Materials.Array.data[0]). Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("-j, --json", "Output as JSON").option("-p, --project <path>", "Unity project path (for asset reference resolution)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, file_id, property, value, options) => {
13326
14954
  if (property === "m_RootOrder") {
13327
14955
  const num = Number(value);
13328
14956
  if (!Number.isInteger(num) || num < 0) {
@@ -13331,6 +14959,8 @@ function build_update_command(getScanner) {
13331
14959
  return;
13332
14960
  }
13333
14961
  }
14962
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection, options.project))
14963
+ return;
13334
14964
  const result = editComponentByFileId({
13335
14965
  file_path: file,
13336
14966
  file_id,
@@ -13342,7 +14972,7 @@ function build_update_command(getScanner) {
13342
14972
  if (!result.success)
13343
14973
  process.exitCode = 1;
13344
14974
  });
13345
- cmd.command("transform <file> <identifier>").description("Edit Transform by GameObject name or transform fileID").option("-p, --position <x,y,z>", "Set local position").option("-r, --rotation <x,y,z>", "Set local rotation (Euler angles in degrees)").option("-s, --scale <x,y,z>", "Set local scale").option("-j, --json", "Output as JSON").action((file, identifier, options) => {
14975
+ cmd.command("transform <file> <identifier>").description("Edit Transform by GameObject name or transform fileID. Use --by-id to force numeric fileID mode").option("-p, --position <x,y,z>", "Set local position").option("-r, --rotation <x,y,z>", "Set local rotation (Euler angles in degrees)").option("-s, --scale <x,y,z>", "Set local scale").option("--by-id", "Treat identifier as a numeric fileID instead of name lookup").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, identifier, options) => {
13346
14976
  if (!options.position && !options.rotation && !options.scale) {
13347
14977
  console.log(JSON.stringify({
13348
14978
  success: false,
@@ -13352,7 +14982,9 @@ function build_update_command(getScanner) {
13352
14982
  process.exitCode = 1;
13353
14983
  return;
13354
14984
  }
13355
- const resolved = resolve_transform_id(getScanner(), file, identifier);
14985
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
14986
+ return;
14987
+ const resolved = resolve_transform_id(getScanner(), file, identifier, options.byId);
13356
14988
  if ("error" in resolved) {
13357
14989
  console.log(JSON.stringify({
13358
14990
  success: false,
@@ -13404,13 +15036,14 @@ function build_update_command(getScanner) {
13404
15036
  if (!result.success)
13405
15037
  process.exitCode = 1;
13406
15038
  });
13407
- cmd.command("settings <project_path>").description("Edit a property in any ProjectSettings/*.asset file").option("-s, --setting <name>", "Setting name or alias").option("--property <name>", "Property name to edit").option("--value <value>", "New value").option("-j, --json", "Output as JSON").action((project_path, options) => {
15039
+ cmd.command("settings").description("Edit a property in any ProjectSettings/*.asset file").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-s, --setting <name>", "Setting name or alias").option("--property <name>", "Property name to edit").option("--value <value>", "New value").option("-j, --json", "Output as JSON").action((options) => {
13408
15040
  if (!options.setting || !options.property || !options.value) {
13409
15041
  console.log(JSON.stringify({ success: false, error: "Required: --setting, --property, --value" }, null, 2));
13410
15042
  process.exit(1);
13411
15043
  }
15044
+ const resolvedProjectPath = resolve_project_path(options.project);
13412
15045
  const result = edit_settings({
13413
- project_path,
15046
+ project_path: resolvedProjectPath,
13414
15047
  setting: options.setting,
13415
15048
  property: options.property,
13416
15049
  value: options.value
@@ -13419,13 +15052,14 @@ function build_update_command(getScanner) {
13419
15052
  if (!result.success)
13420
15053
  process.exitCode = 1;
13421
15054
  });
13422
- cmd.command("tag <project_path> <action> <tag>").description("Add or remove a tag in the TagManager").option("-j, --json", "Output as JSON").action((project_path, action, tag, _options) => {
15055
+ cmd.command("tag <action> <tag>").description("Add or remove a tag in the TagManager").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((action, tag, options) => {
13423
15056
  if (action !== "add" && action !== "remove") {
13424
15057
  console.log(JSON.stringify({ success: false, error: 'Action must be "add" or "remove"' }, null, 2));
13425
15058
  process.exit(1);
13426
15059
  }
15060
+ const resolvedProjectPath = resolve_project_path(options.project);
13427
15061
  const result = edit_tag({
13428
- project_path,
15062
+ project_path: resolvedProjectPath,
13429
15063
  action,
13430
15064
  tag
13431
15065
  });
@@ -13433,9 +15067,10 @@ function build_update_command(getScanner) {
13433
15067
  if (!result.success)
13434
15068
  process.exitCode = 1;
13435
15069
  });
13436
- cmd.command("layer <project_path> <index> <name>").description("Set a named layer at a specific index (3-31)").option("-j, --json", "Output as JSON").action((project_path, index, name, _options) => {
15070
+ cmd.command("layer <index> <name>").description("Set a named layer at a specific index (3-31)").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((index, name, options) => {
15071
+ const resolvedProjectPath = resolve_project_path(options.project);
13437
15072
  const result = edit_layer({
13438
- project_path,
15073
+ project_path: resolvedProjectPath,
13439
15074
  index: parseInt(index, 10),
13440
15075
  name
13441
15076
  });
@@ -13443,13 +15078,14 @@ function build_update_command(getScanner) {
13443
15078
  if (!result.success)
13444
15079
  process.exitCode = 1;
13445
15080
  });
13446
- cmd.command("sorting-layer <project_path> <action> <name>").description("Add or remove a sorting layer").option("-j, --json", "Output as JSON").action((project_path, action, name, _options) => {
15081
+ cmd.command("sorting-layer <action> <name>").description("Add or remove a sorting layer").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((action, name, options) => {
13447
15082
  if (action !== "add" && action !== "remove") {
13448
15083
  console.log(JSON.stringify({ success: false, error: 'Action must be "add" or "remove"' }, null, 2));
13449
15084
  process.exit(1);
13450
15085
  }
15086
+ const resolvedProjectPath = resolve_project_path(options.project);
13451
15087
  const result = edit_sorting_layer({
13452
- project_path,
15088
+ project_path: resolvedProjectPath,
13453
15089
  action,
13454
15090
  name
13455
15091
  });
@@ -13457,7 +15093,9 @@ function build_update_command(getScanner) {
13457
15093
  if (!result.success)
13458
15094
  process.exitCode = 1;
13459
15095
  });
13460
- cmd.command("parent <file> <object_name> <new_parent>").description('Move a GameObject under a new parent. Use "root" to move to scene root. Use --by-id to specify fileIDs instead of names').option("-j, --json", "Output as JSON").option("--by-id", "Treat object_name and new_parent as numeric fileIDs instead of names").action((file, object_name, new_parent, options) => {
15096
+ cmd.command("parent <file> <object_name> <new_parent>").description('Move a GameObject under a new parent. Use "root" to move to scene root. Use --by-id to specify fileIDs instead of names').option("-j, --json", "Output as JSON").option("--by-id", "Treat object_name and new_parent as numeric fileIDs instead of names").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, object_name, new_parent, options) => {
15097
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15098
+ return;
13461
15099
  const result = reparentGameObject({
13462
15100
  file_path: file,
13463
15101
  object_name,
@@ -13469,7 +15107,9 @@ function build_update_command(getScanner) {
13469
15107
  process.exitCode = 1;
13470
15108
  });
13471
15109
  const prefab_cmd = new Command("prefab").description("Prefab operations (unpack, override, remove-override, remove/restore component/gameobject)");
13472
- prefab_cmd.command("unpack <file> <prefab_instance>").description("Unpack a PrefabInstance into standalone GameObjects").option("-p, --project <path>", "Unity project path (for GUID cache lookup)").option("-j, --json", "Output as JSON").action((file, prefab_instance, options) => {
15110
+ prefab_cmd.command("unpack <file> <prefab_instance>").description("Unpack a PrefabInstance into standalone GameObjects").option("-p, --project <path>", "Unity project path (for GUID cache lookup)").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, options) => {
15111
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection, options.project))
15112
+ return;
13473
15113
  const result = unpackPrefab({
13474
15114
  file_path: file,
13475
15115
  prefab_instance,
@@ -13479,7 +15119,9 @@ function build_update_command(getScanner) {
13479
15119
  if (!result.success)
13480
15120
  process.exitCode = 1;
13481
15121
  });
13482
- prefab_cmd.command("override <file> <prefab_instance> <property_path> <value>").description("Edit or add a property override in a PrefabInstance m_Modifications list. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--object-reference <ref>", "Object reference value (default: {fileID: 0})").option("--managed-reference <id>", "Managed reference ID -- placed in value: field, forces objectReference: {fileID: 0}").option("--target <target>", 'Target reference for new entries (e.g., "{fileID: 400000, guid: abc, type: 3}")').option("-j, --json", "Output as JSON").action((file, prefab_instance, property_path, value, options) => {
15122
+ prefab_cmd.command("override <file> <prefab_instance> <property_path> <value>").description("Edit or add a property override in a PrefabInstance m_Modifications list. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--object-reference <ref>", "Object reference value (default: {fileID: 0})").option("--managed-reference <id>", "Managed reference ID -- placed in value: field, forces objectReference: {fileID: 0}").option("--target <target>", 'Target reference for new entries (e.g., "{fileID: 400000, guid: abc, type: 3}")').option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, property_path, value, options) => {
15123
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15124
+ return;
13483
15125
  if (options.objectReference && !/^\{fileID:/.test(options.objectReference)) {
13484
15126
  console.error(`Warning: --object-reference "${options.objectReference}" does not look like a Unity object reference ({fileID: ...}). For managed reference IDs, use --managed-reference instead or pass the ID as the <value> argument.`);
13485
15127
  }
@@ -13496,7 +15138,9 @@ function build_update_command(getScanner) {
13496
15138
  if (!result.success)
13497
15139
  process.exitCode = 1;
13498
15140
  });
13499
- prefab_cmd.command("batch-overrides <file> <prefab_instance> <edits_json>").description('Batch edit multiple property overrides in a PrefabInstance. JSON format: [{"property_path":"...","value":"...","target":"...","object_reference":"..."}]').option("-j, --json", "Output as JSON").action((file, prefab_instance, edits_json, _options) => {
15141
+ prefab_cmd.command("batch-overrides <file> <prefab_instance> <edits_json>").description('Batch edit multiple property overrides in a PrefabInstance. JSON format: [{"property_path":"...","value":"...","target":"...","object_reference":"..."}]').option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, edits_json, options) => {
15142
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15143
+ return;
13500
15144
  let raw_edits;
13501
15145
  try {
13502
15146
  const parsed = JSON.parse(edits_json);
@@ -13535,7 +15179,9 @@ function build_update_command(getScanner) {
13535
15179
  if (!result.success)
13536
15180
  process.exitCode = 1;
13537
15181
  });
13538
- prefab_cmd.command("managed-reference <file> <prefab_instance> <field_path> <type_name>").description("Add a [SerializeReference] managed reference to a prefab override. Auto-generates rid and creates type/data/version override entries.").requiredOption("--target <ref>", 'Target reference (e.g., "{fileID: 400000, guid: ..., type: 3}")').option("--index <n>", "Array index (default: 0)", "0").option("-p, --project <path>", "Unity project path (for type registry)").option("-j, --json", "Output as JSON").action((file, prefab_instance, field_path, type_name, options) => {
15182
+ prefab_cmd.command("managed-reference <file> <prefab_instance> <field_path> <type_name>").description("Add a [SerializeReference] managed reference to a prefab override. Auto-generates rid and creates type/data/version override entries.").requiredOption("--target <ref>", 'Target reference (e.g., "{fileID: 400000, guid: ..., type: 3}")').option("--index <n>", "Array index (default: 0)", "0").option("-p, --project <path>", "Unity project path (for type registry)").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, field_path, type_name, options) => {
15183
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection, options.project))
15184
+ return;
13539
15185
  const result = addPrefabManagedReference({
13540
15186
  file_path: file,
13541
15187
  prefab_instance,
@@ -13549,20 +15195,21 @@ function build_update_command(getScanner) {
13549
15195
  if (!result.success)
13550
15196
  process.exitCode = 1;
13551
15197
  });
13552
- cmd.command("build <project_path> <scene_path>").description("Enable, disable, or move a scene in build settings").option("--enable", "Enable the scene").option("--disable", "Disable the scene").option("--move <index>", "Move scene to position").option("-j, --json", "Output as JSON").action((project_path, scene_path, options) => {
15198
+ cmd.command("build <scene_path>").description("Enable, disable, or move a scene in build settings").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--enable", "Enable the scene").option("--disable", "Disable the scene").option("--move <index>", "Move scene to position").option("-j, --json", "Output as JSON").action((scene_path, options) => {
13553
15199
  try {
15200
+ const resolvedProjectPath = resolve_project_path(options.project);
13554
15201
  if (options.move !== undefined) {
13555
- const result = move_scene(project_path, scene_path, parseInt(options.move, 10));
15202
+ const result = move_scene(resolvedProjectPath, scene_path, parseInt(options.move, 10));
13556
15203
  console.log(JSON.stringify(result, null, 2));
13557
15204
  if (!result.success)
13558
15205
  process.exitCode = 1;
13559
15206
  } else if (options.enable) {
13560
- const result = enable_scene(project_path, scene_path);
15207
+ const result = enable_scene(resolvedProjectPath, scene_path);
13561
15208
  console.log(JSON.stringify(result, null, 2));
13562
15209
  if (!result.success)
13563
15210
  process.exitCode = 1;
13564
15211
  } else if (options.disable) {
13565
- const result = disable_scene(project_path, scene_path);
15212
+ const result = disable_scene(resolvedProjectPath, scene_path);
13566
15213
  console.log(JSON.stringify(result, null, 2));
13567
15214
  if (!result.success)
13568
15215
  process.exitCode = 1;
@@ -13575,7 +15222,7 @@ function build_update_command(getScanner) {
13575
15222
  process.exitCode = 1;
13576
15223
  }
13577
15224
  });
13578
- cmd.command("array <file> <file_id> <array_property> <action> [args...]").description("Insert, append, or remove array elements in a component. Insert: <index> <value> or <value> --index <n>. Append: <value>. Remove: <index> or --index <n>. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--index <n>", "Index for insert/remove").option("-j, --json", "Output as JSON").action((file, file_id, array_property, action, args, options) => {
15225
+ cmd.command("array <file> <file_id> <array_property> <action> [args...]").description("Insert, append, or remove array elements in a component. Insert: <index> <value> or <value> --index <n>. Append: <value>. Remove: <index> or --index <n>. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--index <n>", "Index for insert/remove").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, file_id, array_property, action, args, options) => {
13579
15226
  if (action !== "insert" && action !== "append" && action !== "remove") {
13580
15227
  console.log(JSON.stringify({ success: false, error: 'Action must be "insert", "append", or "remove"' }, null, 2));
13581
15228
  process.exit(1);
@@ -13616,6 +15263,8 @@ function build_update_command(getScanner) {
13616
15263
  }
13617
15264
  }
13618
15265
  }
15266
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15267
+ return;
13619
15268
  const result = editArray({
13620
15269
  file_path: file,
13621
15270
  file_id,
@@ -13628,7 +15277,9 @@ function build_update_command(getScanner) {
13628
15277
  if (!result.success)
13629
15278
  process.exitCode = 1;
13630
15279
  });
13631
- cmd.command("batch <file> <edits_json>").description('Batch edit multiple GameObject properties in a single file operation. JSON format: [{"object_name":"...","property":"...","value":"..."}]').option("-j, --json", "Output as JSON").action((file, edits_json, _options) => {
15280
+ cmd.command("batch <file> <edits_json>").description('Batch edit multiple GameObject properties in a single file operation. JSON format: [{"object_name":"...","property":"...","value":"..."}]').option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, edits_json, options) => {
15281
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15282
+ return;
13632
15283
  let raw_edits;
13633
15284
  try {
13634
15285
  const parsed = JSON.parse(edits_json);
@@ -13669,7 +15320,9 @@ function build_update_command(getScanner) {
13669
15320
  if (!result.success)
13670
15321
  process.exitCode = 1;
13671
15322
  });
13672
- cmd.command("batch-components <file> <edits_json>").description('Batch edit multiple component properties by fileID in a single operation. JSON format: [{"file_id":"...","property":"...","value":"..."}]').option("-j, --json", "Output as JSON").action((file, edits_json, _options) => {
15323
+ cmd.command("batch-components <file> <edits_json>").description('Batch edit multiple component properties by fileID in a single operation. JSON format: [{"file_id":"...","property":"...","value":"..."}]').option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, edits_json, options) => {
15324
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15325
+ return;
13673
15326
  let raw_edits;
13674
15327
  try {
13675
15328
  const parsed = JSON.parse(edits_json);
@@ -13710,7 +15363,9 @@ function build_update_command(getScanner) {
13710
15363
  if (!result.success)
13711
15364
  process.exitCode = 1;
13712
15365
  });
13713
- prefab_cmd.command("remove-override <file> <prefab_instance> <property_path>").description("Remove a property override from a PrefabInstance. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--target <ref>", "Target reference to match (for disambiguation)").option("-j, --json", "Output as JSON").action((file, prefab_instance, property_path, options) => {
15366
+ prefab_cmd.command("remove-override <file> <prefab_instance> <property_path>").description("Remove a property override from a PrefabInstance. Quote paths with brackets or use dot notation (data.0) to avoid shell glob expansion").option("--target <ref>", "Target reference to match (for disambiguation)").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, property_path, options) => {
15367
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15368
+ return;
13714
15369
  const result = removePrefabOverride({
13715
15370
  file_path: file,
13716
15371
  prefab_instance,
@@ -13721,7 +15376,9 @@ function build_update_command(getScanner) {
13721
15376
  if (!result.success)
13722
15377
  process.exitCode = 1;
13723
15378
  });
13724
- prefab_cmd.command("remove-component <file> <prefab_instance> <component_ref>").description("Add a component to the PrefabInstance m_RemovedComponents list").option("-j, --json", "Output as JSON").action((file, prefab_instance, component_ref, _options) => {
15379
+ prefab_cmd.command("remove-component <file> <prefab_instance> <component_ref>").description("Add a component to the PrefabInstance m_RemovedComponents list").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, component_ref, options) => {
15380
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15381
+ return;
13725
15382
  const result = addRemovedComponent({
13726
15383
  file_path: file,
13727
15384
  prefab_instance,
@@ -13731,7 +15388,9 @@ function build_update_command(getScanner) {
13731
15388
  if (!result.success)
13732
15389
  process.exitCode = 1;
13733
15390
  });
13734
- prefab_cmd.command("restore-component <file> <prefab_instance> <component_ref>").description("Remove a component from the PrefabInstance m_RemovedComponents list (restore it)").option("-j, --json", "Output as JSON").action((file, prefab_instance, component_ref, _options) => {
15391
+ prefab_cmd.command("restore-component <file> <prefab_instance> <component_ref>").description("Remove a component from the PrefabInstance m_RemovedComponents list (restore it)").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, component_ref, options) => {
15392
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15393
+ return;
13735
15394
  const result = removeRemovedComponent({
13736
15395
  file_path: file,
13737
15396
  prefab_instance,
@@ -13741,7 +15400,9 @@ function build_update_command(getScanner) {
13741
15400
  if (!result.success)
13742
15401
  process.exitCode = 1;
13743
15402
  });
13744
- prefab_cmd.command("remove-gameobject <file> <prefab_instance> <gameobject_ref>").description("Add a GameObject to the PrefabInstance m_RemovedGameObjects list").option("-j, --json", "Output as JSON").action((file, prefab_instance, gameobject_ref, _options) => {
15403
+ prefab_cmd.command("remove-gameobject <file> <prefab_instance> <gameobject_ref>").description("Add a GameObject to the PrefabInstance m_RemovedGameObjects list").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, gameobject_ref, options) => {
15404
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15405
+ return;
13745
15406
  const result = addRemovedGameObject({
13746
15407
  file_path: file,
13747
15408
  prefab_instance,
@@ -13751,7 +15412,9 @@ function build_update_command(getScanner) {
13751
15412
  if (!result.success)
13752
15413
  process.exitCode = 1;
13753
15414
  });
13754
- prefab_cmd.command("restore-gameobject <file> <prefab_instance> <gameobject_ref>").description("Remove a GameObject from the PrefabInstance m_RemovedGameObjects list (restore it)").option("-j, --json", "Output as JSON").action((file, prefab_instance, gameobject_ref, _options) => {
15415
+ prefab_cmd.command("restore-gameobject <file> <prefab_instance> <gameobject_ref>").description("Remove a GameObject from the PrefabInstance m_RemovedGameObjects list (restore it)").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, prefab_instance, gameobject_ref, options) => {
15416
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
15417
+ return;
13755
15418
  const result = removeRemovedGameObject({
13756
15419
  file_path: file,
13757
15420
  prefab_instance,
@@ -13771,11 +15434,11 @@ function build_update_command(getScanner) {
13771
15434
  }
13772
15435
  }
13773
15436
  }).action((file, options) => {
13774
- if (!import_fs19.existsSync(file)) {
15437
+ if (!import_fs20.existsSync(file)) {
13775
15438
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
13776
15439
  process.exit(1);
13777
15440
  }
13778
- let content = import_fs19.readFileSync(file, "utf-8");
15441
+ let content = import_fs20.readFileSync(file, "utf-8");
13779
15442
  const changes = [];
13780
15443
  if (options.shader) {
13781
15444
  const guid = options.shader;
@@ -13924,7 +15587,7 @@ ${lm[1]} - ${kw}
13924
15587
  process.exitCode = 1;
13925
15588
  return;
13926
15589
  }
13927
- import_fs19.writeFileSync(file, content, "utf-8");
15590
+ import_fs20.writeFileSync(file, content, "utf-8");
13928
15591
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
13929
15592
  });
13930
15593
  cmd.command("meta [file]").description("Edit Unity .meta file importer settings").option("--set <key=value>", "Set an importer setting (e.g., isReadable=1)", (v, p) => [...p, v], []).option("--max-size <n>", "Set TextureImporter maxTextureSize").option("--compression <type>", "Set textureCompression (0=None, 1=LowQuality, 2=Normal, 3=HighQuality)").option("--filter-mode <mode>", "Set filterMode (0=Point, 1=Bilinear, 2=Trilinear)").option("--read-write", "Enable isReadable").option("--no-read-write", "Disable isReadable").option("--batch <glob>", "Apply to all matching files").option("--dry-run", "Preview changes without writing").option("-j, --json", "Output as JSON").action((file, options) => {
@@ -13980,14 +15643,14 @@ ${lm[1]} - ${kw}
13980
15643
  const glob_pattern = options.batch;
13981
15644
  const pattern = glob_pattern.endsWith(".meta") ? glob_pattern : `${glob_pattern}.meta`;
13982
15645
  const first_glob = pattern.search(/[*?[\]{}]/);
13983
- const dir_part = first_glob >= 0 ? pattern.slice(0, pattern.lastIndexOf("/", first_glob)) : import_path9.dirname(pattern);
13984
- const dir = import_path9.resolve(dir_part);
13985
- const suffix = import_path9.basename(pattern).replace(/^\*+/, "");
15646
+ const dir_part = first_glob >= 0 ? pattern.slice(0, pattern.lastIndexOf("/", first_glob)) : import_path12.dirname(pattern);
15647
+ const dir = import_path12.resolve(dir_part);
15648
+ const suffix = import_path12.basename(pattern).replace(/^\*+/, "");
13986
15649
  const meta_files = [];
13987
15650
  const scan = (d) => {
13988
- for (const entry of import_fs19.readdirSync(d)) {
13989
- const full = import_path9.join(d, entry);
13990
- if (import_fs19.statSync(full).isDirectory()) {
15651
+ for (const entry of import_fs20.readdirSync(d)) {
15652
+ const full = import_path12.join(d, entry);
15653
+ if (import_fs20.statSync(full).isDirectory()) {
13991
15654
  if (pattern.includes("**"))
13992
15655
  scan(full);
13993
15656
  } else if (entry.endsWith(suffix)) {
@@ -13995,7 +15658,7 @@ ${lm[1]} - ${kw}
13995
15658
  }
13996
15659
  }
13997
15660
  };
13998
- if (import_fs19.existsSync(dir))
15661
+ if (import_fs20.existsSync(dir))
13999
15662
  scan(dir);
14000
15663
  if (meta_files.length === 0) {
14001
15664
  console.log(JSON.stringify({ success: false, error: `No files matched pattern: ${glob_pattern}` }, null, 2));
@@ -14003,7 +15666,7 @@ ${lm[1]} - ${kw}
14003
15666
  }
14004
15667
  const results = [];
14005
15668
  for (const mf of meta_files) {
14006
- let mc = import_fs19.readFileSync(mf, "utf-8");
15669
+ let mc = import_fs20.readFileSync(mf, "utf-8");
14007
15670
  const file_changes = [];
14008
15671
  for (const edit of edits) {
14009
15672
  const re = new RegExp(`^(\\s*${edit.key}:)[ \\t]*.*$`, "m");
@@ -14014,7 +15677,7 @@ ${lm[1]} - ${kw}
14014
15677
  }
14015
15678
  if (file_changes.length > 0) {
14016
15679
  if (!options.dryRun)
14017
- import_fs19.writeFileSync(mf, mc, "utf-8");
15680
+ import_fs20.writeFileSync(mf, mc, "utf-8");
14018
15681
  results.push({ file: mf, changes: file_changes });
14019
15682
  }
14020
15683
  }
@@ -14027,11 +15690,11 @@ ${lm[1]} - ${kw}
14027
15690
  }, null, 2));
14028
15691
  return;
14029
15692
  }
14030
- if (!import_fs19.existsSync(metaPath)) {
15693
+ if (!import_fs20.existsSync(metaPath)) {
14031
15694
  console.log(JSON.stringify({ success: false, error: `Meta file not found: ${metaPath}` }, null, 2));
14032
15695
  process.exit(1);
14033
15696
  }
14034
- let content = import_fs19.readFileSync(metaPath, "utf-8");
15697
+ let content = import_fs20.readFileSync(metaPath, "utf-8");
14035
15698
  const changes = [];
14036
15699
  for (const edit of edits) {
14037
15700
  const re = new RegExp(`^(\\s*${edit.key}:)[ \\t]*.*$`, "m");
@@ -14046,11 +15709,11 @@ ${lm[1]} - ${kw}
14046
15709
  console.log(JSON.stringify({ success: true, dry_run: true, file: metaPath, changes }, null, 2));
14047
15710
  return;
14048
15711
  }
14049
- import_fs19.writeFileSync(metaPath, content, "utf-8");
15712
+ import_fs20.writeFileSync(metaPath, content, "utf-8");
14050
15713
  console.log(JSON.stringify({ success: true, file: metaPath, changes }, null, 2));
14051
15714
  });
14052
15715
  cmd.command("animation <file>").description("Edit AnimationClip settings and events").option("--set <property=value>", "Set a clip property (e.g., wrap-mode=2 for Loop)", (v, p) => [...p, v], []).option("--add-event <time,function[,data]>", "Add an animation event (e.g., 0.5,OnFootstep,left)", (v, p) => [...p, v], []).option("--remove-event <index>", "Remove an animation event by index (0-based)").option("-j, --json", "Output as JSON").action((file, options) => {
14053
- if (!import_fs19.existsSync(file)) {
15716
+ if (!import_fs20.existsSync(file)) {
14054
15717
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
14055
15718
  process.exit(1);
14056
15719
  }
@@ -14058,7 +15721,7 @@ ${lm[1]} - ${kw}
14058
15721
  console.log(JSON.stringify({ success: false, error: `File is not an AnimationClip (.anim): ${file}` }, null, 2));
14059
15722
  process.exit(1);
14060
15723
  }
14061
- let content = import_fs19.readFileSync(file, "utf-8");
15724
+ let content = import_fs20.readFileSync(file, "utf-8");
14062
15725
  const changes = [];
14063
15726
  const property_map = {
14064
15727
  "wrap-mode": "m_WrapMode",
@@ -14219,16 +15882,16 @@ ${event_yaml}
14219
15882
  process.exitCode = 1;
14220
15883
  return;
14221
15884
  }
14222
- import_fs19.writeFileSync(file, content, "utf-8");
15885
+ import_fs20.writeFileSync(file, content, "utf-8");
14223
15886
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
14224
15887
  });
14225
15888
  const PARAM_TYPE_MAP = { float: 1, int: 3, bool: 4, trigger: 9 };
14226
15889
  cmd.command("animator <file>").description("Edit AnimatorController parameters").option("--add-parameter <name>", "Add a new parameter").option("--type <float|int|bool|trigger>", "Parameter type (required with --add-parameter)").option("--remove-parameter <name>", "Remove a parameter by name").option("--set-default <param=value>", "Set parameter default value (e.g., Speed=1.5)", (v, p) => [...p, v], []).option("-j, --json", "Output as JSON").action((file, options) => {
14227
- if (!import_fs19.existsSync(file)) {
15890
+ if (!import_fs20.existsSync(file)) {
14228
15891
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
14229
15892
  process.exit(1);
14230
15893
  }
14231
- let content = import_fs19.readFileSync(file, "utf-8");
15894
+ let content = import_fs20.readFileSync(file, "utf-8");
14232
15895
  const had_crlf = content.includes(`\r
14233
15896
  `);
14234
15897
  if (had_crlf)
@@ -14330,17 +15993,19 @@ $1`);
14330
15993
  if (had_crlf)
14331
15994
  content = content.replace(/\n/g, `\r
14332
15995
  `);
14333
- import_fs19.writeFileSync(file, content, "utf-8");
15996
+ import_fs20.writeFileSync(file, content, "utf-8");
14334
15997
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
14335
15998
  });
14336
- cmd.command("sibling-index <file> <object_name> <index>").description("Set the sibling index of a GameObject, renumbering all siblings").option("-j, --json", "Output as JSON").action((file, object_name, index_str, _options) => {
15999
+ cmd.command("sibling-index <file> <object_name> <index>").description("Set the sibling index of a GameObject, renumbering all siblings").option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, object_name, index_str, options) => {
14337
16000
  const target_index = parseInt(index_str, 10);
14338
16001
  if (!Number.isFinite(target_index) || target_index < 0) {
14339
16002
  console.log(JSON.stringify({ success: false, error: "Index must be a non-negative integer" }, null, 2));
14340
16003
  process.exitCode = 1;
14341
16004
  return;
14342
16005
  }
14343
- if (!import_fs19.existsSync(file)) {
16006
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection))
16007
+ return;
16008
+ if (!import_fs20.existsSync(file)) {
14344
16009
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
14345
16010
  process.exitCode = 1;
14346
16011
  return;
@@ -14535,7 +16200,7 @@ $1`);
14535
16200
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
14536
16201
  });
14537
16202
  cmd.command("animation-curves <file>").description("Add, remove, or modify animation curves in an .anim file").option("--add-curve <json>", 'Add a curve (JSON: {"type":"float","path":"Body","attribute":"m_Alpha","classID":23,"keyframes":[{"time":0,"value":1}]})').option("--remove-curve <spec>", "Remove a curve by path:attribute (e.g., Body/Mesh:m_Alpha)").option("--set-keyframes <json>", 'Replace keyframes (JSON: {"curve":"path:attribute","keyframes":[{"time":0,"value":10}]})').option("-j, --json", "Output as JSON").action((file, options) => {
14538
- if (!import_fs19.existsSync(file)) {
16203
+ if (!import_fs20.existsSync(file)) {
14539
16204
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
14540
16205
  process.exitCode = 1;
14541
16206
  return;
@@ -14545,7 +16210,7 @@ $1`);
14545
16210
  process.exitCode = 1;
14546
16211
  return;
14547
16212
  }
14548
- let content = import_fs19.readFileSync(file, "utf-8");
16213
+ let content = import_fs20.readFileSync(file, "utf-8");
14549
16214
  const had_crlf = content.includes(`\r
14550
16215
  `);
14551
16216
  if (had_crlf)
@@ -14843,16 +16508,16 @@ ${curve_yaml}`);
14843
16508
  if (had_crlf)
14844
16509
  content = content.replace(/\n/g, `\r
14845
16510
  `);
14846
- import_fs19.writeFileSync(file, content, "utf-8");
16511
+ import_fs20.writeFileSync(file, content, "utf-8");
14847
16512
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
14848
16513
  });
14849
16514
  cmd.command("animator-state <file>").description("Add/remove states and transitions in an AnimatorController").option("--add-state <name>", "Add a new AnimatorState").option("--motion <guid-or-path>", "Motion clip GUID or file path (companion to --add-state)").option("--layer <name>", "Target layer name (default: first layer)").option("--speed <n>", "State speed (companion to --add-state, default: 1)").option("--remove-state <name>", "Remove a state and all its transitions").option("--add-transition <src:dst>", 'Add a transition (use "any" for AnyState source)').option("--condition <param,mode,threshold>", "Transition condition (repeatable)", (v, p) => [...p, v], []).option("--has-exit-time", "Enable exit time on transition").option("--exit-time <n>", "Exit time value (default: 0.75)").option("--duration <n>", "Transition duration (default: 0.25)").option("--remove-transition <src:dst>", "Remove a transition by source:destination state names").option("--set-default-state <name>", "Set the default state in the state machine").option("-j, --json", "Output as JSON").action((file, options) => {
14850
- if (!import_fs19.existsSync(file)) {
16515
+ if (!import_fs20.existsSync(file)) {
14851
16516
  console.log(JSON.stringify({ success: false, error: `File not found: ${file}` }, null, 2));
14852
16517
  process.exitCode = 1;
14853
16518
  return;
14854
16519
  }
14855
- let content = import_fs19.readFileSync(file, "utf-8");
16520
+ let content = import_fs20.readFileSync(file, "utf-8");
14856
16521
  const had_crlf = content.includes(`\r
14857
16522
  `);
14858
16523
  if (had_crlf)
@@ -15191,11 +16856,11 @@ $1`);
15191
16856
  if (had_crlf)
15192
16857
  content = content.replace(/\n/g, `\r
15193
16858
  `);
15194
- import_fs19.writeFileSync(file, content, "utf-8");
16859
+ import_fs20.writeFileSync(file, content, "utf-8");
15195
16860
  console.log(JSON.stringify({ success: true, file, changes }, null, 2));
15196
16861
  });
15197
16862
  cmd.addCommand(prefab_cmd);
15198
- cmd.command("managed-reference <file> <component_id> <field_path> <type_name>").description('Add a managed reference (SerializeReference) to a component field. Type can be "Namespace.ClassName" (registry lookup) or "Assembly Namespace.ClassName" (manual).').option("-p, --project <path>", "Unity project path (for type registry)").option("--append", "Append to array (do not update field rid)").option("--properties <json>", `JSON object of initial field values for the data block (e.g. '{"damage": "10"}')`).option("-j, --json", "Output as JSON").action((file, component_id, field_path, type_name, options) => {
16863
+ cmd.command("managed-reference <file> <component_id> <field_path> <type_name>").description('Add a managed reference (SerializeReference) to a component field. Type can be "Namespace.ClassName" (registry lookup) or "Assembly Namespace.ClassName" (manual).').option("-p, --project <path>", "Unity project path (for type registry)").option("--append", "Append to array (do not update field rid)").option("--properties <json>", `JSON object of initial field values for the data block (e.g. '{"damage": "10"}')`).option("-j, --json", "Output as JSON").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").action(async (file, component_id, field_path, type_name, options) => {
15199
16864
  let initial_values;
15200
16865
  if (options.properties) {
15201
16866
  try {
@@ -15206,6 +16871,8 @@ $1`);
15206
16871
  return;
15207
16872
  }
15208
16873
  }
16874
+ if (!await check_loaded_edit_protection(file, options.bypassLoadedProtection, options.project))
16875
+ return;
15209
16876
  const result = editManagedReference({
15210
16877
  file_path: file,
15211
16878
  file_id: component_id,
@@ -15225,7 +16892,13 @@ $1`);
15225
16892
  // src/cmd-delete.ts
15226
16893
  function build_delete_command() {
15227
16894
  const cmd = new Command("delete").description("Delete Unity objects (GameObjects, components)");
15228
- cmd.command("gameobject <file> <object_name>").description("Delete a GameObject and its hierarchy from a Unity file").option("-j, --json", "Output as JSON").action((file, object_name, _options) => {
16895
+ cmd.command("gameobject <file> <object_name>").description("Delete a GameObject and its hierarchy from a Unity file").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, object_name, options) => {
16896
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
16897
+ if (!guard.allowed) {
16898
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
16899
+ process.exitCode = 1;
16900
+ return;
16901
+ }
15229
16902
  const result = deleteGameObject({
15230
16903
  file_path: file,
15231
16904
  object_name
@@ -15234,10 +16907,22 @@ function build_delete_command() {
15234
16907
  if (!result.success)
15235
16908
  process.exitCode = 1;
15236
16909
  });
15237
- cmd.command("component <file_or_project> <component>").description('Remove a component by file ID or type name (e.g., "Rigidbody", "StandaloneInputModule")').option("--on <game_object>", "Scope to a specific GameObject (name or fileID)").option("-p, --project <path>", "Unity project path (for script GUID lookup)").option("--all", "Remove from all scenes and prefabs in the project (first arg becomes project path)").option("-j, --json", "Output as JSON").action((file_or_project, component, options) => {
16910
+ cmd.command("component <file_or_project> <component>").description('Remove a component by file ID or type name (e.g., "Rigidbody", "StandaloneInputModule")').option("--on <game_object>", "Scope to a specific GameObject (name or fileID)").option("-p, --project <path>", "Unity project path (for script GUID lookup)").option("--all", "Remove from all scenes and prefabs in the project (first arg becomes project path)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file_or_project, component, options) => {
15238
16911
  if (options.all) {
16912
+ const resolvedProjectPath = resolve_project_path(file_or_project);
16913
+ if (!options.bypassLoadedProtection) {
16914
+ const connected = await is_editor_connected_for_project(resolvedProjectPath);
16915
+ if (connected) {
16916
+ console.log(JSON.stringify({
16917
+ success: false,
16918
+ error: "Refusing to run project-wide delete while editor is connected. Use --bypass-loaded-protection to force file-based edits."
16919
+ }, null, 2));
16920
+ process.exitCode = 1;
16921
+ return;
16922
+ }
16923
+ }
15239
16924
  const result = removeComponentBatch({
15240
- project_path: file_or_project,
16925
+ project_path: resolvedProjectPath,
15241
16926
  component_type: component,
15242
16927
  game_object: options.on
15243
16928
  });
@@ -15245,6 +16930,12 @@ function build_delete_command() {
15245
16930
  if (!result.success)
15246
16931
  process.exitCode = 1;
15247
16932
  } else {
16933
+ const guard = await enforce_loaded_edit_protection(file_or_project, options.bypassLoadedProtection, options.project);
16934
+ if (!guard.allowed) {
16935
+ console.log(JSON.stringify({ success: false, file_path: file_or_project, error: guard.error }, null, 2));
16936
+ process.exitCode = 1;
16937
+ return;
16938
+ }
15248
16939
  const result = removeComponent({
15249
16940
  file_path: file_or_project,
15250
16941
  file_id: component,
@@ -15256,9 +16947,10 @@ function build_delete_command() {
15256
16947
  process.exitCode = 1;
15257
16948
  }
15258
16949
  });
15259
- cmd.command("build <project_path> <scene_path>").description("Remove a scene from build settings").option("-j, --json", "Output as JSON").action((project_path, scene_path, _options) => {
16950
+ cmd.command("build <scene_path>").description("Remove a scene from build settings").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((scene_path, options) => {
16951
+ const resolvedProjectPath = resolve_project_path(options.project);
15260
16952
  try {
15261
- const result = remove_scene(project_path, scene_path);
16953
+ const result = remove_scene(resolvedProjectPath, scene_path);
15262
16954
  console.log(JSON.stringify(result, null, 2));
15263
16955
  if (!result.success)
15264
16956
  process.exitCode = 1;
@@ -15267,7 +16959,13 @@ function build_delete_command() {
15267
16959
  process.exitCode = 1;
15268
16960
  }
15269
16961
  });
15270
- cmd.command("prefab <file> <prefab_instance>").description("Delete a PrefabInstance and all its stripped/added blocks from a Unity file").option("-j, --json", "Output as JSON").action((file, prefab_instance, _options) => {
16962
+ cmd.command("prefab <file> <prefab_instance>").description("Delete a PrefabInstance and all its stripped/added blocks from a Unity file").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, prefab_instance, options) => {
16963
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
16964
+ if (!guard.allowed) {
16965
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
16966
+ process.exitCode = 1;
16967
+ return;
16968
+ }
15271
16969
  const result = deletePrefabInstance({
15272
16970
  file_path: file,
15273
16971
  prefab_instance
@@ -15276,9 +16974,22 @@ function build_delete_command() {
15276
16974
  if (!result.success)
15277
16975
  process.exitCode = 1;
15278
16976
  });
15279
- cmd.command("package <project_path> <name>").description("Remove a package from Packages/manifest.json").option("-j, --json", "Output as JSON").action((project_path, name, _options) => {
16977
+ cmd.command("asset <file>").description("Delete an asset file and its .meta sidecar (missing .meta => warning)").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, options) => {
16978
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
16979
+ if (!guard.allowed) {
16980
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
16981
+ process.exitCode = 1;
16982
+ return;
16983
+ }
16984
+ const result = deleteAssetFile({ file_path: file });
16985
+ console.log(JSON.stringify(result, null, 2));
16986
+ if (!result.success)
16987
+ process.exitCode = 1;
16988
+ });
16989
+ cmd.command("package <name>").description("Remove a package from Packages/manifest.json").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((name, options) => {
15280
16990
  try {
15281
- const result = remove_package(project_path, name);
16991
+ const resolvedProjectPath = resolve_project_path(options.project);
16992
+ const result = remove_package(resolvedProjectPath, name);
15282
16993
  if ("error" in result) {
15283
16994
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15284
16995
  process.exitCode = 1;
@@ -15294,278 +17005,11 @@ function build_delete_command() {
15294
17005
  }
15295
17006
 
15296
17007
  // src/cmd-editor.ts
15297
- var import_path11 = require("path");
15298
-
15299
- // src/editor-client.ts
15300
- var import_fs20 = require("fs");
15301
- var import_path10 = require("path");
15302
- function read_editor_config(project_path) {
15303
- const config_path = import_path10.join(project_path, ".unity-agentic", "editor.json");
15304
- if (!import_fs20.existsSync(config_path)) {
15305
- return { error: `Editor bridge not found at ${config_path}. Is the Unity Editor running with the bridge package installed?` };
15306
- }
15307
- let config;
15308
- try {
15309
- const raw = import_fs20.readFileSync(config_path, "utf-8");
15310
- config = JSON.parse(raw);
15311
- } catch (err) {
15312
- return { error: `Failed to parse editor.json: ${err instanceof Error ? err.message : String(err)}` };
15313
- }
15314
- if (typeof config.port !== "number" || typeof config.pid !== "number") {
15315
- return { error: "Invalid editor.json: missing port or pid" };
15316
- }
15317
- if (!is_pid_alive(config.pid)) {
15318
- return { error: `Unity Editor process (PID ${config.pid}) is not running. The editor may have been closed.` };
15319
- }
15320
- return config;
15321
- }
15322
- var RETRYABLE_CODES = new Set([-32000, -32002, -32003]);
15323
- var RETRY_DELAYS = [500, 1000, 2000];
15324
- async function call_editor(options) {
15325
- const maxRetries = options.retries ?? 2;
15326
- let lastResponse;
15327
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
15328
- lastResponse = await call_editor_once(options);
15329
- if (!lastResponse.error || !RETRYABLE_CODES.has(lastResponse.error.code)) {
15330
- return lastResponse;
15331
- }
15332
- if (attempt < maxRetries) {
15333
- const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)];
15334
- await new Promise((r) => setTimeout(r, delay));
15335
- }
15336
- }
15337
- return lastResponse;
15338
- }
15339
- function call_editor_once(options) {
15340
- const { method, params, timeout = 1e4, no_wait } = options;
15341
- const config = resolve_config(options);
15342
- if ("error" in config) {
15343
- return Promise.resolve({
15344
- jsonrpc: "2.0",
15345
- id: "0",
15346
- error: { code: -32000, message: config.error }
15347
- });
15348
- }
15349
- const url = `ws://127.0.0.1:${config.port}/unity-agentic`;
15350
- const request_id = generate_id();
15351
- const wire_params = { ...params };
15352
- if (timeout !== 1e4)
15353
- wire_params._timeout = timeout;
15354
- if (no_wait)
15355
- wire_params.no_wait = true;
15356
- const request = {
15357
- jsonrpc: "2.0",
15358
- id: request_id,
15359
- method,
15360
- ...Object.keys(wire_params).length > 0 ? { params: wire_params } : {}
15361
- };
15362
- return new Promise((resolve7) => {
15363
- let resolved = false;
15364
- let ws;
15365
- const timer = setTimeout(() => {
15366
- if (!resolved) {
15367
- resolved = true;
15368
- try {
15369
- ws?.close();
15370
- } catch {}
15371
- resolve7({
15372
- jsonrpc: "2.0",
15373
- id: request_id,
15374
- error: { code: -32001, message: `Timeout after ${timeout}ms waiting for response to ${method}` }
15375
- });
15376
- }
15377
- }, timeout);
15378
- try {
15379
- ws = new WebSocket(url);
15380
- ws.onopen = () => {
15381
- ws.send(JSON.stringify(request));
15382
- if (no_wait && !resolved) {
15383
- resolved = true;
15384
- clearTimeout(timer);
15385
- setTimeout(() => {
15386
- try {
15387
- ws.close();
15388
- } catch {}
15389
- }, 200);
15390
- resolve7({
15391
- jsonrpc: "2.0",
15392
- id: request_id,
15393
- result: { queued: true }
15394
- });
15395
- }
15396
- };
15397
- ws.onmessage = (event) => {
15398
- try {
15399
- const data = JSON.parse(String(event.data));
15400
- if (data.id === request_id) {
15401
- if (!resolved) {
15402
- resolved = true;
15403
- clearTimeout(timer);
15404
- ws.close();
15405
- resolve7(data);
15406
- }
15407
- }
15408
- } catch {}
15409
- };
15410
- ws.onerror = () => {
15411
- if (!resolved) {
15412
- resolved = true;
15413
- clearTimeout(timer);
15414
- resolve7({
15415
- jsonrpc: "2.0",
15416
- id: request_id,
15417
- error: { code: -32002, message: `WebSocket connection failed to ${url}. Is the Unity Editor running?` }
15418
- });
15419
- }
15420
- };
15421
- ws.onclose = () => {
15422
- if (!resolved) {
15423
- resolved = true;
15424
- clearTimeout(timer);
15425
- resolve7({
15426
- jsonrpc: "2.0",
15427
- id: request_id,
15428
- error: { code: -32003, message: "WebSocket connection closed before response received" }
15429
- });
15430
- }
15431
- };
15432
- } catch (err) {
15433
- if (!resolved) {
15434
- resolved = true;
15435
- clearTimeout(timer);
15436
- resolve7({
15437
- jsonrpc: "2.0",
15438
- id: request_id,
15439
- error: { code: -32002, message: `Failed to connect: ${err instanceof Error ? err.message : String(err)}` }
15440
- });
15441
- }
15442
- }
15443
- });
15444
- }
15445
- async function stream_editor(options) {
15446
- const { method, params, timeout = 30000, on_event } = options;
15447
- const config = resolve_config(options);
15448
- if ("error" in config) {
15449
- throw new Error(config.error);
15450
- }
15451
- const MAX_RECONNECTS = 5;
15452
- let reconnect_count = 0;
15453
- let stopped = false;
15454
- return new Promise((resolve7, reject) => {
15455
- let resolved = false;
15456
- function connect(url) {
15457
- const ws = new WebSocket(url);
15458
- const request_id = generate_id();
15459
- const request = {
15460
- jsonrpc: "2.0",
15461
- id: request_id,
15462
- method,
15463
- ...params ? { params } : {}
15464
- };
15465
- let connected = false;
15466
- const timer = !resolved ? setTimeout(() => {
15467
- if (!connected && !resolved) {
15468
- reject(new Error(`Timeout connecting to ${url}`));
15469
- }
15470
- }, timeout) : null;
15471
- ws.onopen = () => {
15472
- connected = true;
15473
- if (timer)
15474
- clearTimeout(timer);
15475
- reconnect_count = 0;
15476
- ws.send(JSON.stringify(request));
15477
- if (!resolved) {
15478
- resolved = true;
15479
- resolve7({ close: () => {
15480
- stopped = true;
15481
- ws.close();
15482
- } });
15483
- }
15484
- };
15485
- ws.onmessage = (event) => {
15486
- try {
15487
- const data = JSON.parse(String(event.data));
15488
- if ("method" in data && !("id" in data)) {
15489
- on_event(data);
15490
- }
15491
- } catch {}
15492
- };
15493
- ws.onerror = () => {
15494
- if (!connected && !resolved) {
15495
- if (timer)
15496
- clearTimeout(timer);
15497
- reject(new Error(`WebSocket connection failed to ${url}`));
15498
- }
15499
- };
15500
- ws.onclose = () => {
15501
- if (stopped)
15502
- return;
15503
- if (!connected && !resolved)
15504
- return;
15505
- if (reconnect_count >= MAX_RECONNECTS)
15506
- return;
15507
- reconnect_count++;
15508
- const delay = Math.min(500 * reconnect_count, 3000);
15509
- setTimeout(() => {
15510
- if (stopped)
15511
- return;
15512
- const fresh_config = resolve_config(options);
15513
- if ("error" in fresh_config)
15514
- return;
15515
- connect(`ws://127.0.0.1:${fresh_config.port}/unity-agentic`);
15516
- }, delay);
15517
- };
15518
- }
15519
- connect(`ws://127.0.0.1:${config.port}/unity-agentic`);
15520
- });
15521
- }
15522
- async function ping_editor(port, timeout_ms = 2000) {
15523
- return new Promise((resolve7) => {
15524
- const timer = setTimeout(() => {
15525
- resolve7({ reachable: false, error: `Timeout after ${timeout_ms}ms` });
15526
- }, timeout_ms);
15527
- try {
15528
- const ws = new WebSocket(`ws://127.0.0.1:${port}/unity-agentic`);
15529
- ws.onopen = () => {
15530
- clearTimeout(timer);
15531
- try {
15532
- ws.close();
15533
- } catch {}
15534
- resolve7({ reachable: true });
15535
- };
15536
- ws.onerror = (err) => {
15537
- clearTimeout(timer);
15538
- resolve7({ reachable: false, error: String(err) });
15539
- };
15540
- } catch (err) {
15541
- clearTimeout(timer);
15542
- resolve7({ reachable: false, error: String(err) });
15543
- }
15544
- });
15545
- }
15546
- function resolve_config(options) {
15547
- if (options.port) {
15548
- return { port: options.port, pid: 0, version: "unknown" };
15549
- }
15550
- return read_editor_config(options.project_path);
15551
- }
15552
- function is_pid_alive(pid) {
15553
- try {
15554
- process.kill(pid, 0);
15555
- return true;
15556
- } catch {
15557
- return false;
15558
- }
15559
- }
15560
- function generate_id() {
15561
- return `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
15562
- }
15563
-
15564
- // src/cmd-editor.ts
17008
+ var import_path13 = require("path");
15565
17009
  var BRIDGE_PACKAGE_NAME = "com.unity-agentic-tools.editor-bridge";
15566
17010
  var BRIDGE_PACKAGE_VERSION = "https://github.com/taconotsandwich/unity-agentic-tools.git?path=unity-package";
15567
17011
  function get_common_options(cmd) {
15568
- let current = cmd.parent;
17012
+ let current = cmd;
15569
17013
  let project;
15570
17014
  let timeout_str;
15571
17015
  let port_str;
@@ -15579,7 +17023,7 @@ function get_common_options(cmd) {
15579
17023
  port_str = opts.port;
15580
17024
  current = current.parent;
15581
17025
  }
15582
- const project_path = import_path11.resolve(project || process.cwd());
17026
+ const project_path = import_path13.resolve(project || process.cwd());
15583
17027
  const timeout = parseInt(timeout_str || "10000", 10);
15584
17028
  const port = port_str ? parseInt(port_str, 10) : undefined;
15585
17029
  return { project_path, timeout, port };
@@ -15757,9 +17201,9 @@ function build_editor_command() {
15757
17201
  params.mode = options.mode;
15758
17202
  await handle_rpc(this, "editor.tests.run", params);
15759
17203
  });
15760
- cmd.command("install <project_path>").description("Install the editor bridge package into a Unity project").action((_project_path) => {
15761
- const resolved_path = import_path11.resolve(_project_path);
15762
- const result = add_package(resolved_path, BRIDGE_PACKAGE_NAME, BRIDGE_PACKAGE_VERSION);
17204
+ cmd.command("install").description("Install the editor bridge package into a Unity project").option("-p, --project <path>", "Path to Unity project (defaults to cwd)").action(function() {
17205
+ const { project_path } = get_common_options(this);
17206
+ const result = add_package(project_path, BRIDGE_PACKAGE_NAME, BRIDGE_PACKAGE_VERSION);
15763
17207
  if ("error" in result) {
15764
17208
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15765
17209
  process.exitCode = 1;
@@ -15767,9 +17211,9 @@ function build_editor_command() {
15767
17211
  }
15768
17212
  console.log(JSON.stringify(result, null, 2));
15769
17213
  });
15770
- cmd.command("uninstall <project_path>").description("Remove the editor bridge package from a Unity project").action((_project_path) => {
15771
- const resolved_path = import_path11.resolve(_project_path);
15772
- const result = remove_package(resolved_path, BRIDGE_PACKAGE_NAME);
17214
+ cmd.command("uninstall").description("Remove the editor bridge package from a Unity project").option("-p, --project <path>", "Path to Unity project (defaults to cwd)").action(function() {
17215
+ const { project_path } = get_common_options(this);
17216
+ const result = remove_package(project_path, BRIDGE_PACKAGE_NAME);
15773
17217
  if ("error" in result) {
15774
17218
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15775
17219
  process.exitCode = 1;
@@ -15994,7 +17438,13 @@ program.addCommand(build_read_command(getScanner));
15994
17438
  program.addCommand(build_update_command(getScanner));
15995
17439
  program.addCommand(build_delete_command());
15996
17440
  program.addCommand(build_editor_command());
15997
- program.command("clone <file> <object_name>").description("Duplicate a GameObject and its hierarchy").option("-n, --name <new_name>", "Name for the duplicated object").option("-j, --json", "Output as JSON").action((file, object_name, options) => {
17441
+ program.command("clone <file> <object_name>").description("Duplicate a GameObject and its hierarchy").option("-n, --name <new_name>", "Name for the duplicated object").option("--bypass-loaded-protection", "Allow editing files currently loaded in Unity Editor").option("-j, --json", "Output as JSON").action(async (file, object_name, options) => {
17442
+ const guard = await enforce_loaded_edit_protection(file, options.bypassLoadedProtection);
17443
+ if (!guard.allowed) {
17444
+ console.log(JSON.stringify({ success: false, file_path: file, error: guard.error }, null, 2));
17445
+ process.exitCode = 1;
17446
+ return;
17447
+ }
15998
17448
  const result = duplicateGameObject({
15999
17449
  file_path: file,
16000
17450
  object_name,
@@ -16100,11 +17550,12 @@ program.command("search <path> [pattern]").description("Search for GameObjects.
16100
17550
  console.log(JSON.stringify(result, null, 2));
16101
17551
  }
16102
17552
  });
16103
- program.command("grep <project_path> <pattern>").description("Search for a regex pattern across project files").option("--type <type>", "File type filter: cs, yaml, unity, prefab, asset, all", "all").option("-m, --max <n>", "Max results (default: 100)", "100").option("-C, --context <n>", "Context lines around matches", "0").option("-j, --json", "Output as JSON").action((project_path, pattern, options) => {
17553
+ program.command("grep <pattern>").description("Search for a regex pattern across project files").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("--type <type>", "File type filter: cs, yaml, unity, prefab, asset, all", "all").option("-m, --max <n>", "Max results (default: 100)", "100").option("-C, --context <n>", "Context lines around matches", "0").option("-j, --json", "Output as JSON").action((pattern, options) => {
16104
17554
  if (!pattern || pattern.trim() === "") {
16105
17555
  console.log(JSON.stringify({ success: false, error: "Pattern must not be empty" }, null, 2));
16106
17556
  process.exit(1);
16107
17557
  }
17558
+ let project_path = resolve_project_path(options.project);
16108
17559
  const abs_path = path9.resolve(project_path);
16109
17560
  const project_root = find_unity_project_root(abs_path);
16110
17561
  if (project_root) {
@@ -16132,8 +17583,9 @@ program.command("grep <project_path> <pattern>").description("Search for a regex
16132
17583
  if (!result.success)
16133
17584
  process.exitCode = 1;
16134
17585
  });
16135
- program.command("version <project_path>").description("Read Unity project version").option("-j, --json", "Output as JSON").action((project_path, _options) => {
17586
+ program.command("version").description("Read Unity project version").option("-p, --project <path>", "Unity project path (defaults to cwd)").option("-j, --json", "Output as JSON").action((options) => {
16136
17587
  try {
17588
+ const project_path = resolve_project_path(options.project);
16137
17589
  const version = read_project_version(project_path);
16138
17590
  console.log(JSON.stringify(version, null, 2));
16139
17591
  } catch (err) {