unity-agentic-tools 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0",
2302
+ version: "0.4.2",
2303
2303
  description: "Fast, token-efficient Unity YAML parser for AI agents",
2304
2304
  exports: {
2305
2305
  ".": {
@@ -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)) {
@@ -3286,9 +3289,75 @@ function resolveScriptGuid(script, projectPath) {
3286
3289
  }
3287
3290
  } catch {}
3288
3291
  }
3292
+ try {
3293
+ const assetsDir = path.join(projectPath, "Assets");
3294
+ if (import_fs6.existsSync(assetsDir)) {
3295
+ const entries = import_fs6.readdirSync(assetsDir, { recursive: true, withFileTypes: false });
3296
+ for (const entry of entries) {
3297
+ if (!entry.endsWith(".cs"))
3298
+ continue;
3299
+ const fileName = path.basename(entry, ".cs").toLowerCase();
3300
+ if (fileName === scriptNameLower || classNameOnly && fileName === classNameOnly) {
3301
+ const fullPath = path.join(assetsDir, entry);
3302
+ const guid = extractGuidFromMeta(fullPath + ".meta");
3303
+ if (guid) {
3304
+ return { guid, path: path.join("Assets", entry) };
3305
+ }
3306
+ }
3307
+ }
3308
+ }
3309
+ } catch {}
3289
3310
  }
3290
3311
  return null;
3291
3312
  }
3313
+ var ASSET_PPTR_MAP = {
3314
+ ".inputactions": { fileID: 11400000, type: 2 },
3315
+ ".asset": { fileID: 11400000, type: 2 },
3316
+ ".mat": { fileID: 2100000, type: 2 },
3317
+ ".prefab": { fileID: 100100000, type: 2 },
3318
+ ".controller": { fileID: 9100000, type: 2 },
3319
+ ".anim": { fileID: 7400000, type: 2 }
3320
+ };
3321
+ var ASSET_EXTENSIONS = new Set(Object.keys(ASSET_PPTR_MAP));
3322
+ function resolveAssetPathToPPtr(value, file_path, project_path) {
3323
+ if (value.startsWith("{fileID:") || value.startsWith("{"))
3324
+ return;
3325
+ const ext = path.extname(value).toLowerCase();
3326
+ if (!ASSET_EXTENSIONS.has(ext))
3327
+ return;
3328
+ const resolved_project = project_path || find_unity_project_root(path.dirname(file_path));
3329
+ if (!resolved_project)
3330
+ return null;
3331
+ const mapping = ASSET_PPTR_MAP[ext];
3332
+ let guid = null;
3333
+ const basename_val = path.basename(value, ext);
3334
+ if (value.includes("/") || value.includes("\\")) {
3335
+ const abs = path.isAbsolute(value) ? value : path.join(resolved_project, value);
3336
+ guid = extractGuidFromMeta(abs + ".meta");
3337
+ }
3338
+ if (!guid) {
3339
+ const cache = load_guid_cache_for_file(file_path, resolved_project);
3340
+ if (cache) {
3341
+ const found = cache.find_by_name(basename_val, ext);
3342
+ if (found)
3343
+ guid = found.guid;
3344
+ }
3345
+ }
3346
+ if (!guid) {
3347
+ const candidates = [
3348
+ path.join(resolved_project, "Assets", value),
3349
+ path.join(resolved_project, value)
3350
+ ];
3351
+ for (const candidate of candidates) {
3352
+ guid = extractGuidFromMeta(candidate + ".meta");
3353
+ if (guid)
3354
+ break;
3355
+ }
3356
+ }
3357
+ if (!guid)
3358
+ return null;
3359
+ return `{fileID: ${mapping.fileID}, guid: ${guid}, type: ${mapping.type}}`;
3360
+ }
3292
3361
  function resolve_script_with_fields(script, project_path) {
3293
3362
  const resolved = resolveScriptGuid(script, project_path);
3294
3363
  if (!resolved)
@@ -3392,8 +3461,8 @@ function build_type_lookup(project_path) {
3392
3461
  const assetsDir = path.join(project_path, "Assets");
3393
3462
  const csIndex = new Map;
3394
3463
  try {
3395
- const { readdirSync: readdirSync3 } = require("fs");
3396
- const entries = readdirSync3(assetsDir, { recursive: true, withFileTypes: false });
3464
+ const { readdirSync: readdirSync4 } = require("fs");
3465
+ const entries = readdirSync4(assetsDir, { recursive: true, withFileTypes: false });
3397
3466
  for (const entry of entries) {
3398
3467
  if (typeof entry === "string" && entry.endsWith(".cs")) {
3399
3468
  const basename3 = entry.substring(entry.lastIndexOf("/") + 1).replace(/\.cs$/, "");
@@ -3845,7 +3914,9 @@ ${element_indent2}- ${value}`);
3845
3914
  if (!header) {
3846
3915
  throw new Error(`Invalid Unity YAML block header in replacement text: "${new_text.slice(0, 80)}"`);
3847
3916
  }
3848
- this._raw = new_text;
3917
+ this._raw = new_text.endsWith(`
3918
+ `) ? new_text : new_text + `
3919
+ `;
3849
3920
  this._header = header;
3850
3921
  this._dirty = true;
3851
3922
  this._format_cache.clear();
@@ -3855,6 +3926,11 @@ ${element_indent2}- ${value}`);
3855
3926
  }
3856
3927
  _check_dirty(old_raw) {
3857
3928
  if (this._raw !== old_raw) {
3929
+ if (!this._raw.endsWith(`
3930
+ `)) {
3931
+ this._raw += `
3932
+ `;
3933
+ }
3858
3934
  this._dirty = true;
3859
3935
  return true;
3860
3936
  }
@@ -4937,14 +5013,82 @@ function get_project_info(projectPath) {
4937
5013
  }
4938
5014
 
4939
5015
  // src/editor/create.ts
5016
+ function md4(data) {
5017
+ function F(x, y, z) {
5018
+ return x & y | ~x & z;
5019
+ }
5020
+ function G(x, y, z) {
5021
+ return x & y | x & z | y & z;
5022
+ }
5023
+ function H(x, y, z) {
5024
+ return x ^ y ^ z;
5025
+ }
5026
+ function rotl(x, n) {
5027
+ return (x << n | x >>> 32 - n) >>> 0;
5028
+ }
5029
+ const len = data.length;
5030
+ const bitLen = len * 8;
5031
+ const padded_len = len + 9 + 63 & ~63;
5032
+ const buf = Buffer.alloc(padded_len);
5033
+ data.copy(buf);
5034
+ buf[len] = 128;
5035
+ buf.writeUInt32LE(bitLen >>> 0, padded_len - 8);
5036
+ buf.writeUInt32LE(Math.floor(bitLen / 4294967296) >>> 0, padded_len - 4);
5037
+ let a0 = 1732584193;
5038
+ let b0 = 4023233417;
5039
+ let c0 = 2562383102;
5040
+ let d0 = 271733878;
5041
+ const R1_K = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
5042
+ const R1_S = [3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19];
5043
+ const R2_K = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15];
5044
+ const R2_S = [3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13];
5045
+ const R3_K = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
5046
+ const R3_S = [3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15];
5047
+ for (let i = 0;i < padded_len; i += 64) {
5048
+ const X = [];
5049
+ for (let j = 0;j < 16; j++)
5050
+ X[j] = buf.readUInt32LE(i + j * 4);
5051
+ let a = a0, b = b0, c = c0, d = d0;
5052
+ for (let j = 0;j < 16; j++) {
5053
+ const t = a + F(b, c, d) + X[R1_K[j]] >>> 0;
5054
+ a = d;
5055
+ d = c;
5056
+ c = b;
5057
+ b = rotl(t, R1_S[j]);
5058
+ }
5059
+ for (let j = 0;j < 16; j++) {
5060
+ const t = a + G(b, c, d) + X[R2_K[j]] + 1518500249 >>> 0;
5061
+ a = d;
5062
+ d = c;
5063
+ c = b;
5064
+ b = rotl(t, R2_S[j]);
5065
+ }
5066
+ for (let j = 0;j < 16; j++) {
5067
+ const t = a + H(b, c, d) + X[R3_K[j]] + 1859775393 >>> 0;
5068
+ a = d;
5069
+ d = c;
5070
+ c = b;
5071
+ b = rotl(t, R3_S[j]);
5072
+ }
5073
+ a0 = a0 + a >>> 0;
5074
+ b0 = b0 + b >>> 0;
5075
+ c0 = c0 + c >>> 0;
5076
+ d0 = d0 + d >>> 0;
5077
+ }
5078
+ const result = Buffer.alloc(16);
5079
+ result.writeUInt32LE(a0, 0);
5080
+ result.writeUInt32LE(b0, 4);
5081
+ result.writeUInt32LE(c0, 8);
5082
+ result.writeUInt32LE(d0, 12);
5083
+ return result;
5084
+ }
4940
5085
  function compute_dll_script_file_id(namespace, class_name) {
4941
- const { createHash } = require("crypto");
4942
5086
  const full_name = namespace ? `${namespace}.${class_name}` : class_name;
4943
5087
  const name_bytes = Buffer.from(full_name, "utf-8");
4944
5088
  const input = Buffer.alloc(4 + name_bytes.length);
4945
5089
  input.writeInt32LE(114, 0);
4946
5090
  name_bytes.copy(input, 4);
4947
- const hash = createHash("md4").update(input).digest();
5091
+ const hash = md4(input);
4948
5092
  const a = hash.readInt32LE(0);
4949
5093
  const b = hash.readInt32LE(4);
4950
5094
  const c = hash.readInt32LE(8);
@@ -4957,13 +5101,13 @@ function get_layer_from_parent(doc, parentTransformId) {
4957
5101
  const parentTransform = doc.find_by_file_id(parentTransformId);
4958
5102
  if (!parentTransform)
4959
5103
  return 0;
4960
- const goMatch = parentTransform.raw.match(/m_GameObject:\s*\{fileID:\s*(\d+)\}/);
5104
+ const goMatch = parentTransform.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(\d+)\}/);
4961
5105
  if (!goMatch)
4962
5106
  return 0;
4963
5107
  const parentGo = doc.find_by_file_id(goMatch[1]);
4964
5108
  if (!parentGo)
4965
5109
  return 0;
4966
- const layerMatch = parentGo.raw.match(/m_Layer:\s*(\d+)/);
5110
+ const layerMatch = parentGo.raw.match(/m_Layer:[ \t]*(\d+)/);
4967
5111
  return layerMatch ? parseInt(layerMatch[1], 10) : 0;
4968
5112
  }
4969
5113
  function createGameObjectYAML(gameObjectId, transformId, name, parentTransformId = "0", rootOrder = 0, layer = 0) {
@@ -5100,48 +5244,320 @@ function find_game_object_in_variant(doc, name, file_path, project_path) {
5100
5244
  }
5101
5245
  return null;
5102
5246
  }
5103
- function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
5104
- const piBlock = doc.find_by_file_id(piId);
5105
- if (!piBlock)
5106
- return;
5107
- const entry = `
5108
- - targetCorrespondingSourceObject: ${targetSourceRef}
5109
- insertIndex: -1
5110
- addedObject: {fileID: ${newGoId}}`;
5111
- let raw = piBlock.raw;
5112
- const emptyPattern = /m_AddedGameObjects:[ \t]*\[\]/;
5113
- if (emptyPattern.test(raw)) {
5114
- raw = raw.replace(emptyPattern, `m_AddedGameObjects:${entry}`);
5115
- piBlock.replace_raw(raw);
5116
- return;
5247
+ function parsePrefabRef(refText) {
5248
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
5249
+ if (!fileIdMatch)
5250
+ return null;
5251
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
5252
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
5253
+ return {
5254
+ file_id: fileIdMatch[1],
5255
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
5256
+ type: typeMatch ? typeMatch[1] : undefined
5257
+ };
5258
+ }
5259
+ function extractPrefabSourceGuid(prefabInstanceRaw) {
5260
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
5261
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
5262
+ }
5263
+ function normalizePrefabRef(refText, sourceGuid) {
5264
+ const parsed = parsePrefabRef(refText);
5265
+ if (!parsed) {
5266
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
5267
+ }
5268
+ const guid = parsed.guid ?? sourceGuid;
5269
+ if (!guid) {
5270
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
5271
+ }
5272
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
5273
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
5117
5274
  }
5118
- const existingPattern = /(m_AddedGameObjects:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
5119
- if (existingPattern.test(raw)) {
5120
- raw = raw.replace(existingPattern, `$1${entry}`);
5121
- piBlock.replace_raw(raw);
5275
+ const type = parsed.type ?? "3";
5276
+ if (!/^-?\d+$/.test(type)) {
5277
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
5122
5278
  }
5279
+ return {
5280
+ ref: {
5281
+ file_id: parsed.file_id,
5282
+ guid,
5283
+ type
5284
+ }
5285
+ };
5123
5286
  }
5124
- function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
5125
- const piBlock = doc.find_by_file_id(piId);
5126
- if (!piBlock)
5127
- return;
5128
- const entry = `
5129
- - targetCorrespondingSourceObject: ${targetSourceRef}
5130
- insertIndex: -1
5131
- addedObject: {fileID: ${newComponentId}}`;
5132
- let raw = piBlock.raw;
5133
- const emptyPattern = /m_AddedComponents:[ \t]*\[\]/;
5134
- if (emptyPattern.test(raw)) {
5135
- raw = raw.replace(emptyPattern, `m_AddedComponents:${entry}`);
5136
- piBlock.replace_raw(raw);
5137
- return;
5287
+ function serializePrefabRef(ref) {
5288
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
5289
+ }
5290
+ function ensurePrefabModificationSerializedVersion(prefabInstanceBlock) {
5291
+ const raw = prefabInstanceBlock.raw;
5292
+ const updated = raw.replace(/(^([ \t]*)m_Modification:[ \t]*\n)(?!\2[ \t]+serializedVersion:[ \t]*\d+)/m, (_match, prefix, indent) => `${prefix}${indent} serializedVersion: 3
5293
+ `);
5294
+ if (updated === raw) {
5295
+ return false;
5296
+ }
5297
+ prefabInstanceBlock.replace_raw(updated);
5298
+ return true;
5299
+ }
5300
+ function addedOverrideEntryKey(entry) {
5301
+ return `${entry.target.file_id}|${entry.target.guid}|${entry.target.type}|${entry.insert_index}|${entry.added_object_file_id}`;
5302
+ }
5303
+ function splitPrefabArraySection(rawText, key) {
5304
+ const lines = rawText.split(`
5305
+ `);
5306
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
5307
+ let keyIndex = -1;
5308
+ let indent = "";
5309
+ for (let i = 0;i < lines.length; i++) {
5310
+ const match = lines[i].match(keyPattern);
5311
+ if (match) {
5312
+ keyIndex = i;
5313
+ indent = match[1];
5314
+ break;
5315
+ }
5316
+ }
5317
+ if (keyIndex === -1) {
5318
+ return null;
5319
+ }
5320
+ const keyIndentLen = indent.length;
5321
+ let sectionEnd = keyIndex + 1;
5322
+ for (let i = keyIndex + 1;i < lines.length; i++) {
5323
+ const line = lines[i];
5324
+ const trimmed = line.trim();
5325
+ if (trimmed.length === 0) {
5326
+ sectionEnd = i + 1;
5327
+ continue;
5328
+ }
5329
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
5330
+ if (indentLen < keyIndentLen) {
5331
+ break;
5332
+ }
5333
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
5334
+ break;
5335
+ }
5336
+ sectionEnd = i + 1;
5337
+ }
5338
+ return {
5339
+ lines,
5340
+ key_index: keyIndex,
5341
+ section_end: sectionEnd,
5342
+ indent
5343
+ };
5344
+ }
5345
+ function collectAddedOverrideEntries(lines, key_index, section_end, sourceGuid) {
5346
+ const sectionLines = lines.slice(key_index + 1, section_end);
5347
+ const dedup = new Map;
5348
+ let i = 0;
5349
+ while (i < sectionLines.length) {
5350
+ const line = sectionLines[i];
5351
+ const trimmed = line.trimStart();
5352
+ if (!trimmed.startsWith("-")) {
5353
+ i++;
5354
+ continue;
5355
+ }
5356
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
5357
+ const entryLines = [line];
5358
+ i++;
5359
+ while (i < sectionLines.length) {
5360
+ const nextLine = sectionLines[i];
5361
+ const nextTrimmed = nextLine.trim();
5362
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
5363
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
5364
+ break;
5365
+ }
5366
+ entryLines.push(nextLine);
5367
+ i++;
5368
+ }
5369
+ const entryText = entryLines.join(`
5370
+ `);
5371
+ const targetMatch = entryText.match(/targetCorrespondingSourceObject:[ \t]*(\{[^}]+\})/);
5372
+ const insertMatch = entryText.match(/insertIndex:[ \t]*(-?\d+)/);
5373
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
5374
+ if (!targetMatch || !addedObjectMatch) {
5375
+ continue;
5376
+ }
5377
+ const normalizedTarget = normalizePrefabRef(targetMatch[1], sourceGuid).ref;
5378
+ if (!normalizedTarget) {
5379
+ continue;
5380
+ }
5381
+ const insertIndex = insertMatch ? Number.parseInt(insertMatch[1], 10) : -1;
5382
+ const entry = {
5383
+ target: normalizedTarget,
5384
+ insert_index: Number.isNaN(insertIndex) ? -1 : insertIndex,
5385
+ added_object_file_id: addedObjectMatch[1]
5386
+ };
5387
+ const dedupKey = addedOverrideEntryKey(entry);
5388
+ if (!dedup.has(dedupKey)) {
5389
+ dedup.set(dedupKey, entry);
5390
+ }
5138
5391
  }
5139
- const existingPattern = /(m_AddedComponents:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
5140
- if (existingPattern.test(raw)) {
5141
- raw = raw.replace(existingPattern, `$1${entry}`);
5142
- piBlock.replace_raw(raw);
5392
+ return [...dedup.values()];
5393
+ }
5394
+ function appendToAddedOverrideArray(doc, piId, key, targetSourceRef, newObjectId) {
5395
+ const piBlock = doc.find_by_file_id(piId);
5396
+ if (!piBlock) {
5397
+ return { success: false, error: `PrefabInstance with fileID ${piId} not found` };
5398
+ }
5399
+ ensurePrefabModificationSerializedVersion(piBlock);
5400
+ const split = splitPrefabArraySection(piBlock.raw, key);
5401
+ if (!split) {
5402
+ return { success: false, error: `${key} property not found in PrefabInstance ${piId}` };
5403
+ }
5404
+ const sourceGuid = extractPrefabSourceGuid(piBlock.raw);
5405
+ const normalizedTarget = normalizePrefabRef(targetSourceRef, sourceGuid);
5406
+ if (!normalizedTarget.ref) {
5407
+ return { success: false, error: normalizedTarget.error };
5408
+ }
5409
+ const candidate = {
5410
+ target: normalizedTarget.ref,
5411
+ insert_index: -1,
5412
+ added_object_file_id: newObjectId
5413
+ };
5414
+ const candidateKey = addedOverrideEntryKey(candidate);
5415
+ const existing = collectAddedOverrideEntries(split.lines, split.key_index, split.section_end, sourceGuid);
5416
+ if (existing.some((entry) => addedOverrideEntryKey(entry) === candidateKey)) {
5417
+ return { success: true, changed: false };
5418
+ }
5419
+ const entryLines = [
5420
+ `${split.indent}- targetCorrespondingSourceObject: ${serializePrefabRef(candidate.target)}`,
5421
+ `${split.indent} insertIndex: ${candidate.insert_index}`,
5422
+ `${split.indent} addedObject: {fileID: ${candidate.added_object_file_id}}`
5423
+ ];
5424
+ const keyLine = split.lines[split.key_index].trim();
5425
+ let insertIndex = split.section_end;
5426
+ while (insertIndex > split.key_index + 1 && split.lines[insertIndex - 1].trim().length === 0) {
5427
+ insertIndex--;
5428
+ }
5429
+ let updatedLines;
5430
+ if (keyLine === `${key}: []`) {
5431
+ const replacement = [`${split.indent}${key}:`, ...entryLines];
5432
+ updatedLines = [
5433
+ ...split.lines.slice(0, split.key_index),
5434
+ ...replacement,
5435
+ ...split.lines.slice(split.section_end)
5436
+ ];
5437
+ } else {
5438
+ updatedLines = [
5439
+ ...split.lines.slice(0, insertIndex),
5440
+ ...entryLines,
5441
+ ...split.lines.slice(insertIndex)
5442
+ ];
5143
5443
  }
5444
+ piBlock.replace_raw(updatedLines.join(`
5445
+ `));
5446
+ return { success: true, changed: true };
5447
+ }
5448
+ function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
5449
+ return appendToAddedOverrideArray(doc, piId, "m_AddedGameObjects", targetSourceRef, newGoId);
5144
5450
  }
5451
+ function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
5452
+ return appendToAddedOverrideArray(doc, piId, "m_AddedComponents", targetSourceRef, newComponentId);
5453
+ }
5454
+ var COMPONENT_FULL_TEMPLATES = {
5455
+ 65: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
5456
+ m_Material: {fileID: 0}
5457
+ m_IncludeLayers:
5458
+ serializedVersion: 2
5459
+ m_Bits: 0
5460
+ m_ExcludeLayers:
5461
+ serializedVersion: 2
5462
+ m_Bits: 0
5463
+ m_LayerOverridePriority: 0
5464
+ m_IsTrigger: 0
5465
+ m_ProvidesContacts: 0
5466
+ m_Enabled: 1
5467
+ serializedVersion: 3
5468
+ m_Size: {x: 1, y: 1, z: 1}
5469
+ m_Center: {x: 0, y: 0, z: 0}`,
5470
+ 82: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
5471
+ m_Enabled: 1
5472
+ serializedVersion: 4
5473
+ OutputAudioMixerGroup: {fileID: 0}
5474
+ m_audioClip: {fileID: 0}
5475
+ m_PlayOnAwake: 1
5476
+ m_Volume: 1
5477
+ m_Pitch: 1
5478
+ Loop: 0
5479
+ Mute: 0
5480
+ Spatialize: 0
5481
+ SpatializePostEffects: 0
5482
+ Priority: 128
5483
+ DopplerLevel: 1
5484
+ MinDistance: 1
5485
+ MaxDistance: 500
5486
+ Pan2D: 0
5487
+ rolloffMode: 0
5488
+ BypassEffects: 0
5489
+ BypassListenerEffects: 0
5490
+ BypassReverbZones: 0
5491
+ rolloffCustomCurve:
5492
+ serializedVersion: 2
5493
+ m_Curve:
5494
+ - serializedVersion: 3
5495
+ time: 0
5496
+ value: 1
5497
+ inSlope: 0
5498
+ outSlope: 0
5499
+ tangentMode: 0
5500
+ weightedMode: 0
5501
+ inWeight: 0.33333334
5502
+ outWeight: 0.33333334
5503
+ - serializedVersion: 3
5504
+ time: 1
5505
+ value: 0
5506
+ inSlope: 0
5507
+ outSlope: 0
5508
+ tangentMode: 0
5509
+ weightedMode: 0
5510
+ inWeight: 0.33333334
5511
+ outWeight: 0.33333334
5512
+ m_PreInfinity: 2
5513
+ m_PostInfinity: 2
5514
+ m_RotationOrder: 4
5515
+ panLevelCustomCurve:
5516
+ serializedVersion: 2
5517
+ m_Curve:
5518
+ - serializedVersion: 3
5519
+ time: 0
5520
+ value: 0
5521
+ inSlope: 0
5522
+ outSlope: 0
5523
+ tangentMode: 0
5524
+ weightedMode: 0
5525
+ inWeight: 0.33333334
5526
+ outWeight: 0.33333334
5527
+ m_PreInfinity: 2
5528
+ m_PostInfinity: 2
5529
+ m_RotationOrder: 4
5530
+ spreadCustomCurve:
5531
+ serializedVersion: 2
5532
+ m_Curve:
5533
+ - serializedVersion: 3
5534
+ time: 0
5535
+ value: 0
5536
+ inSlope: 0
5537
+ outSlope: 0
5538
+ tangentMode: 0
5539
+ weightedMode: 0
5540
+ inWeight: 0.33333334
5541
+ outWeight: 0.33333334
5542
+ m_PreInfinity: 2
5543
+ m_PostInfinity: 2
5544
+ m_RotationOrder: 4
5545
+ reverbZoneMixCustomCurve:
5546
+ serializedVersion: 2
5547
+ m_Curve:
5548
+ - serializedVersion: 3
5549
+ time: 0
5550
+ value: 1
5551
+ inSlope: 0
5552
+ outSlope: 0
5553
+ tangentMode: 0
5554
+ weightedMode: 0
5555
+ inWeight: 0.33333334
5556
+ outWeight: 0.33333334
5557
+ m_PreInfinity: 2
5558
+ m_PostInfinity: 2
5559
+ m_RotationOrder: 4`
5560
+ };
5145
5561
  var COMPONENT_DEFAULTS = {
5146
5562
  20: ` serializedVersion: 2
5147
5563
  m_ClearFlags: 1
@@ -5268,6 +5684,17 @@ var COMPONENT_DEFAULTS = {
5268
5684
  m_IgnoreParentGroups: 0`
5269
5685
  };
5270
5686
  function createGenericComponentYAML(componentName, classId, componentId, gameObjectId) {
5687
+ const fullTemplate = COMPONENT_FULL_TEMPLATES[classId];
5688
+ if (fullTemplate) {
5689
+ return `--- !u!${classId} &${componentId}
5690
+ ${componentName}:
5691
+ m_ObjectHideFlags: 0
5692
+ m_CorrespondingSourceObject: {fileID: 0}
5693
+ m_PrefabInstance: {fileID: 0}
5694
+ m_PrefabAsset: {fileID: 0}
5695
+ ${fullTemplate(gameObjectId)}
5696
+ `;
5697
+ }
5271
5698
  const defaults = COMPONENT_DEFAULTS[classId] ? `
5272
5699
  ` + COMPONENT_DEFAULTS[classId] + `
5273
5700
  ` : "";
@@ -5284,11 +5711,14 @@ ${defaults}`;
5284
5711
  function addComponentToGameObject(doc, gameObjectId, componentId) {
5285
5712
  const goBlock = doc.find_by_file_id(gameObjectId);
5286
5713
  if (!goBlock)
5287
- return;
5288
- let raw = goBlock.raw;
5289
- raw = raw.replace(/(m_Component:\s*\n(?:\s*-\s*component:\s*\{fileID:\s*\d+\}\s*\n)*)/, `$1 - component: {fileID: ${componentId}}
5714
+ return false;
5715
+ const raw = goBlock.raw;
5716
+ const updated = raw.replace(/(m_Component:[ \t]*\n(?:[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*\d+\}[ \t]*\n)*)/, `$1 - component: {fileID: ${componentId}}
5290
5717
  `);
5291
- goBlock.replace_raw(raw);
5718
+ if (updated === raw)
5719
+ return false;
5720
+ goBlock.replace_raw(updated);
5721
+ return true;
5292
5722
  }
5293
5723
  function createMonoBehaviourYAML(componentId, gameObjectId, scriptGuid, fields, version, scriptFileId = 11500000, type_lookup) {
5294
5724
  const field_yaml = fields && fields.length > 0 ? generate_field_yaml(fields, version, " ", type_lookup) : `
@@ -5360,7 +5790,7 @@ function createGameObject(options) {
5360
5790
  if (parentBlock.class_id === 4) {
5361
5791
  parentTransformIdStr = parentIdStr;
5362
5792
  } else if (parentBlock.class_id === 1) {
5363
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
5793
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
5364
5794
  if (compMatch) {
5365
5795
  parentTransformIdStr = compMatch[1];
5366
5796
  } else {
@@ -5417,7 +5847,14 @@ function createGameObject(options) {
5417
5847
  doc.add_child_to_parent(parentTransformIdStr, transformIdStr);
5418
5848
  }
5419
5849
  if (variantPiId && strippedSourceRef) {
5420
- appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
5850
+ const appendResult = appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
5851
+ if (!appendResult.success) {
5852
+ return {
5853
+ success: false,
5854
+ file_path,
5855
+ error: appendResult.error || `Failed to update m_AddedGameObjects for PrefabInstance ${variantPiId}`
5856
+ };
5857
+ }
5421
5858
  }
5422
5859
  const saveResult = doc.save();
5423
5860
  if (!saveResult.success) {
@@ -5900,6 +6337,7 @@ PrefabInstance:
5900
6337
  m_ObjectHideFlags: 0
5901
6338
  serializedVersion: 2
5902
6339
  m_Modification:
6340
+ serializedVersion: 3
5903
6341
  m_TransformParent: {fileID: 0}
5904
6342
  m_Modifications:
5905
6343
  - target: {fileID: ${rootInfo.game_object.file_id}, guid: ${sourceGuid}, type: 3}
@@ -6004,7 +6442,7 @@ function createPrefabInstance(options) {
6004
6442
  if (parentBlock.class_id === 4) {
6005
6443
  parentTransformIdStr = parentIdStr;
6006
6444
  } else if (parentBlock.class_id === 1) {
6007
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
6445
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6008
6446
  if (compMatch) {
6009
6447
  parentTransformIdStr = compMatch[1];
6010
6448
  } else {
@@ -6046,6 +6484,7 @@ PrefabInstance:
6046
6484
  m_ObjectHideFlags: 0
6047
6485
  serializedVersion: 2
6048
6486
  m_Modification:
6487
+ serializedVersion: 3
6049
6488
  m_TransformParent: {fileID: ${parentTransformIdStr}}
6050
6489
  m_Modifications:
6051
6490
  - target: {fileID: ${rootGoFileId}, guid: ${sourceGuid}, type: 3}
@@ -6350,6 +6789,8 @@ function is_valid_component_base(base_class, project_path, depth = 0) {
6350
6789
  }
6351
6790
  if (!fullPath.endsWith(".cs") || !import_fs8.existsSync(fullPath)) {
6352
6791
  if (match.kind === "class") {
6792
+ if (fullPath.includes("Assembly-CSharp.dll"))
6793
+ return true;
6353
6794
  return is_likely_component_base_name(base_class);
6354
6795
  }
6355
6796
  }
@@ -6434,7 +6875,7 @@ function addComponent(options) {
6434
6875
  let duplicateWarning;
6435
6876
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
6436
6877
  if (goBlock && classId !== null) {
6437
- const compRefs = [...goBlock.raw.matchAll(/component:\s*\{fileID:\s*(\d+)\}/g)].map((m) => m[1]);
6878
+ const compRefs = [...goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/g)].map((m) => m[1]);
6438
6879
  for (const refId of compRefs) {
6439
6880
  const compBlock = doc.find_by_file_id(refId);
6440
6881
  if (compBlock && compBlock.class_id === classId) {
@@ -6494,11 +6935,11 @@ function addComponent(options) {
6494
6935
  };
6495
6936
  }
6496
6937
  if (resolved.base_class && !COMPONENT_BASE_CLASSES.has(resolved.base_class) && !is_valid_component_base(resolved.base_class, project_path)) {
6497
- return {
6498
- success: false,
6499
- file_path,
6500
- 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).`
6501
- };
6938
+ 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).`;
6939
+ if (!extractionError)
6940
+ extractionError = warn;
6941
+ else
6942
+ extractionError += " " + warn;
6502
6943
  }
6503
6944
  let version;
6504
6945
  if (project_path) {
@@ -6514,14 +6955,50 @@ function addComponent(options) {
6514
6955
  componentYAML = createMonoBehaviourYAML(componentId, gameObjectId, resolved.guid, resolved.fields, version, scriptFileId, type_lookup_comp);
6515
6956
  scriptGuid = resolved.guid;
6516
6957
  scriptPath = resolved.path || undefined;
6517
- extractionError = resolved.extraction_error;
6958
+ if (resolved.extraction_error) {
6959
+ if (!extractionError)
6960
+ extractionError = resolved.extraction_error;
6961
+ else
6962
+ extractionError += " " + resolved.extraction_error;
6963
+ }
6518
6964
  }
6519
6965
  if (variantInfo) {
6520
- appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
6966
+ const appendResult = appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
6967
+ if (!appendResult.success) {
6968
+ return {
6969
+ success: false,
6970
+ file_path,
6971
+ error: appendResult.error || `Failed to update m_AddedComponents for PrefabInstance ${variantInfo.prefab_instance_id}`
6972
+ };
6973
+ }
6521
6974
  } else {
6522
- addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
6975
+ const added = addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
6976
+ if (!added) {
6977
+ return {
6978
+ success: false,
6979
+ file_path,
6980
+ error: `Failed to add component reference to GameObject ${gameObjectIdStr}: m_Component array not found or malformed`
6981
+ };
6982
+ }
6523
6983
  }
6524
6984
  doc.append_raw(componentYAML);
6985
+ if (!doc.validate()) {
6986
+ return {
6987
+ success: false,
6988
+ file_path,
6989
+ error: "Validation failed after adding component. The generated YAML may be malformed."
6990
+ };
6991
+ }
6992
+ if (!variantInfo) {
6993
+ const goBlockAfter = doc.find_by_file_id(gameObjectIdStr);
6994
+ if (!goBlockAfter || !goBlockAfter.raw.includes(`component: {fileID: ${componentIdStr}}`)) {
6995
+ return {
6996
+ success: false,
6997
+ file_path,
6998
+ error: `Component block appended but reference not found in GameObject ${gameObjectIdStr}'s m_Component list`
6999
+ };
7000
+ }
7001
+ }
6525
7002
  const saveResult = doc.save();
6526
7003
  if (!saveResult.success) {
6527
7004
  return {
@@ -6577,8 +7054,11 @@ function copyComponent(options) {
6577
7054
  const newIdStr = doc.generate_file_id();
6578
7055
  const newId = parseInt(newIdStr, 10);
6579
7056
  let clonedBlock = sourceBlock.raw.replace(new RegExp(`^(--- !u!${sourceBlock.class_id} &)${source_file_id}`), `$1${newId}`);
6580
- clonedBlock = clonedBlock.replace(/m_GameObject:\s*\{fileID:\s*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
6581
- addComponentToGameObject(doc, targetGoIdStr, newIdStr);
7057
+ clonedBlock = clonedBlock.replace(/m_GameObject:[ \t]*\{fileID:[ \t]*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
7058
+ const added = addComponentToGameObject(doc, targetGoIdStr, newIdStr);
7059
+ if (!added) {
7060
+ return { success: false, file_path, error: `Failed to wire component to target GameObject ${targetGoIdStr}: m_Component array not found or malformed` };
7061
+ }
6582
7062
  doc.append_raw(clonedBlock);
6583
7063
  if (!doc.validate()) {
6584
7064
  return { success: false, file_path, error: "Validation failed after copying component" };
@@ -7235,7 +7715,17 @@ function validate_value_type(current_value, new_value) {
7235
7715
  const incoming = new_value.trim();
7236
7716
  if (/^\{fileID:/.test(current)) {
7237
7717
  if (/^\{fileID:[ \t]*0[ \t]*\}$/.test(current)) {
7238
- return null;
7718
+ if (/^\{fileID:/.test(incoming))
7719
+ return null;
7720
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(incoming))
7721
+ return null;
7722
+ if (/^(true|false|yes|no|on|off)$/i.test(incoming))
7723
+ return null;
7724
+ if (/^'[^']*'$/.test(incoming) || /^"[^"]*"$/.test(incoming))
7725
+ return null;
7726
+ if (/^\{.*:.*\}$/.test(incoming))
7727
+ return null;
7728
+ return `Current value is a null reference ({fileID: 0}). New value "${incoming}" is not a valid reference, number, boolean, quoted string, or compound value.`;
7239
7729
  }
7240
7730
  if (!/^\{fileID:/.test(incoming)) {
7241
7731
  return `Expected a reference value ({fileID: ...}), got "${incoming}"`;
@@ -7445,7 +7935,19 @@ function editProperty(options) {
7445
7935
  return result;
7446
7936
  }
7447
7937
  function editComponentByFileId(options) {
7448
- const { file_path, file_id, property, new_value } = options;
7938
+ const { file_path, file_id, property, project_path } = options;
7939
+ let { new_value } = options;
7940
+ const resolved = resolveAssetPathToPPtr(new_value, file_path, project_path);
7941
+ if (resolved === null) {
7942
+ return {
7943
+ success: false,
7944
+ file_path,
7945
+ error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup" first, or "setup -p <path>").`
7946
+ };
7947
+ }
7948
+ if (resolved !== undefined) {
7949
+ new_value = resolved;
7950
+ }
7449
7951
  const pathError = validate_file_path(file_path, "write");
7450
7952
  if (pathError) {
7451
7953
  return { success: false, file_path, error: pathError };
@@ -8017,6 +8519,193 @@ function findPrefabInstanceBlock(doc, identifier) {
8017
8519
  }
8018
8520
  return null;
8019
8521
  }
8522
+ function parsePrefabRef2(refText) {
8523
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
8524
+ if (!fileIdMatch)
8525
+ return null;
8526
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
8527
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
8528
+ return {
8529
+ file_id: fileIdMatch[1],
8530
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
8531
+ type: typeMatch ? typeMatch[1] : undefined
8532
+ };
8533
+ }
8534
+ function extractPrefabSourceGuid2(prefabInstanceRaw) {
8535
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
8536
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
8537
+ }
8538
+ function normalizePrefabRef2(refText, sourceGuid) {
8539
+ const parsed = parsePrefabRef2(refText);
8540
+ if (!parsed) {
8541
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
8542
+ }
8543
+ const guid = parsed.guid ?? sourceGuid;
8544
+ if (!guid) {
8545
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
8546
+ }
8547
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
8548
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
8549
+ }
8550
+ const type = parsed.type ?? "3";
8551
+ if (!/^-?\d+$/.test(type)) {
8552
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
8553
+ }
8554
+ return {
8555
+ ref: {
8556
+ file_id: parsed.file_id,
8557
+ guid,
8558
+ type
8559
+ }
8560
+ };
8561
+ }
8562
+ function parsePrefabRefMatcher(refText) {
8563
+ const parsed = parsePrefabRef2(refText);
8564
+ if (!parsed) {
8565
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
8566
+ }
8567
+ if (parsed.guid && !/^[a-f0-9]{32}$/i.test(parsed.guid)) {
8568
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
8569
+ }
8570
+ if (parsed.type && !/^-?\d+$/.test(parsed.type)) {
8571
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
8572
+ }
8573
+ return { matcher: parsed };
8574
+ }
8575
+ function prefabRefKey(ref) {
8576
+ return `${ref.file_id}|${ref.guid}|${ref.type}`;
8577
+ }
8578
+ function serializePrefabRef2(ref) {
8579
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
8580
+ }
8581
+ function splitPrefabArraySection2(rawText, key) {
8582
+ const lines = rawText.split(`
8583
+ `);
8584
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
8585
+ let keyIndex = -1;
8586
+ let indent = "";
8587
+ for (let i = 0;i < lines.length; i++) {
8588
+ const match = lines[i].match(keyPattern);
8589
+ if (match) {
8590
+ keyIndex = i;
8591
+ indent = match[1];
8592
+ break;
8593
+ }
8594
+ }
8595
+ if (keyIndex === -1) {
8596
+ return null;
8597
+ }
8598
+ const keyIndentLen = indent.length;
8599
+ let sectionEnd = keyIndex + 1;
8600
+ for (let i = keyIndex + 1;i < lines.length; i++) {
8601
+ const line = lines[i];
8602
+ const trimmed = line.trim();
8603
+ if (trimmed.length === 0) {
8604
+ sectionEnd = i + 1;
8605
+ continue;
8606
+ }
8607
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
8608
+ if (indentLen < keyIndentLen) {
8609
+ break;
8610
+ }
8611
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
8612
+ break;
8613
+ }
8614
+ sectionEnd = i + 1;
8615
+ }
8616
+ return {
8617
+ lines,
8618
+ key_index: keyIndex,
8619
+ section_end: sectionEnd,
8620
+ indent
8621
+ };
8622
+ }
8623
+ function collectNormalizedRefsFromSection(lines, key_index, section_end, sourceGuid) {
8624
+ const sectionText = lines.slice(key_index, section_end).join(`
8625
+ `);
8626
+ const refMatches = sectionText.match(/\{[^}]*\}/g) || [];
8627
+ const dedup = new Map;
8628
+ for (const token of refMatches) {
8629
+ const parsed = parsePrefabRef2(token);
8630
+ if (!parsed)
8631
+ continue;
8632
+ const guid = parsed.guid ?? sourceGuid;
8633
+ if (!guid || !/^[a-f0-9]{32}$/i.test(guid))
8634
+ continue;
8635
+ const type = parsed.type ?? "3";
8636
+ if (!/^-?\d+$/.test(type))
8637
+ continue;
8638
+ const normalized = {
8639
+ file_id: parsed.file_id,
8640
+ guid: guid.toLowerCase(),
8641
+ type
8642
+ };
8643
+ const key = prefabRefKey(normalized);
8644
+ if (!dedup.has(key)) {
8645
+ dedup.set(key, normalized);
8646
+ }
8647
+ }
8648
+ return [...dedup.values()];
8649
+ }
8650
+ function mutatePrefabReferenceList(options) {
8651
+ const { target_block, key, ref_text, action, item_label } = options;
8652
+ const split = splitPrefabArraySection2(target_block.raw, key);
8653
+ if (!split) {
8654
+ return { success: false, error: `${key} property not found in PrefabInstance` };
8655
+ }
8656
+ const sourceGuid = extractPrefabSourceGuid2(target_block.raw);
8657
+ const existing = collectNormalizedRefsFromSection(split.lines, split.key_index, split.section_end, sourceGuid);
8658
+ let next = existing;
8659
+ if (action === "add") {
8660
+ const normalized = normalizePrefabRef2(ref_text, sourceGuid);
8661
+ if (!normalized.ref) {
8662
+ return { success: false, error: normalized.error };
8663
+ }
8664
+ const keyText = prefabRefKey(normalized.ref);
8665
+ if (existing.some((entry) => prefabRefKey(entry) === keyText)) {
8666
+ return { success: true, changed: false };
8667
+ }
8668
+ next = [...existing, normalized.ref];
8669
+ } else {
8670
+ const matcherResult = parsePrefabRefMatcher(ref_text);
8671
+ const matcher = matcherResult.matcher;
8672
+ if (!matcher) {
8673
+ return { success: false, error: matcherResult.error };
8674
+ }
8675
+ next = existing.filter((entry) => {
8676
+ if (entry.file_id !== matcher.file_id)
8677
+ return true;
8678
+ if (matcher.guid && entry.guid !== matcher.guid.toLowerCase())
8679
+ return true;
8680
+ if (matcher.type && entry.type !== matcher.type)
8681
+ return true;
8682
+ return false;
8683
+ });
8684
+ if (next.length === existing.length) {
8685
+ return {
8686
+ success: false,
8687
+ error: `${item_label} reference "${ref_text}" not found in ${key}`
8688
+ };
8689
+ }
8690
+ }
8691
+ const replacement = [];
8692
+ if (next.length === 0) {
8693
+ replacement.push(`${split.indent}${key}: []`);
8694
+ } else {
8695
+ replacement.push(`${split.indent}${key}:`);
8696
+ for (const entry of next) {
8697
+ replacement.push(`${split.indent}- ${serializePrefabRef2(entry)}`);
8698
+ }
8699
+ }
8700
+ const updatedLines = [
8701
+ ...split.lines.slice(0, split.key_index),
8702
+ ...replacement,
8703
+ ...split.lines.slice(split.section_end)
8704
+ ];
8705
+ target_block.replace_raw(updatedLines.join(`
8706
+ `));
8707
+ return { success: true, changed: true };
8708
+ }
8020
8709
  function editArray(options) {
8021
8710
  const { file_path, file_id, array_property, action, value, index } = options;
8022
8711
  if (!import_fs10.existsSync(file_path)) {
@@ -8227,9 +8916,6 @@ function addRemovedComponent(options) {
8227
8916
  if (!import_fs10.existsSync(file_path)) {
8228
8917
  return { success: false, file_path, error: `File not found: ${file_path}` };
8229
8918
  }
8230
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
8231
- return { success: false, file_path, error: `Invalid component reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
8232
- }
8233
8919
  let doc;
8234
8920
  try {
8235
8921
  doc = UnityDocument.from_file(file_path);
@@ -8240,19 +8926,16 @@ function addRemovedComponent(options) {
8240
8926
  if (!targetBlock) {
8241
8927
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8242
8928
  }
8243
- let rawText = targetBlock.raw;
8244
- if (/m_RemovedComponents:\s*\[\]/.test(rawText)) {
8245
- rawText = rawText.replace(/m_RemovedComponents:\s*\[\]/, "m_RemovedComponents:");
8246
- }
8247
- const removedPattern = /m_RemovedComponents:\s*\n/;
8248
- if (removedPattern.test(rawText)) {
8249
- rawText = rawText.replace(removedPattern, `m_RemovedComponents:
8250
- - ${component_ref}
8251
- `);
8252
- } else {
8253
- return { success: false, file_path, error: "m_RemovedComponents property not found in PrefabInstance" };
8929
+ const mutation = mutatePrefabReferenceList({
8930
+ target_block: targetBlock,
8931
+ key: "m_RemovedComponents",
8932
+ ref_text: component_ref,
8933
+ action: "add",
8934
+ item_label: "Component"
8935
+ });
8936
+ if (!mutation.success) {
8937
+ return { success: false, file_path, error: mutation.error };
8254
8938
  }
8255
- targetBlock.replace_raw(rawText);
8256
8939
  if (!doc.validate()) {
8257
8940
  return { success: false, file_path, error: "Validation failed after adding removed component" };
8258
8941
  }
@@ -8281,13 +8964,16 @@ function removeRemovedComponent(options) {
8281
8964
  if (!targetBlock) {
8282
8965
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8283
8966
  }
8284
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8285
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
8286
- if (!refPattern.test(targetBlock.raw)) {
8287
- return { success: false, file_path, error: `Component reference "${component_ref}" not found in m_RemovedComponents` };
8967
+ const mutation = mutatePrefabReferenceList({
8968
+ target_block: targetBlock,
8969
+ key: "m_RemovedComponents",
8970
+ ref_text: component_ref,
8971
+ action: "remove",
8972
+ item_label: "Component"
8973
+ });
8974
+ if (!mutation.success) {
8975
+ return { success: false, file_path, error: mutation.error };
8288
8976
  }
8289
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
8290
- targetBlock.replace_raw(updatedRaw);
8291
8977
  if (!doc.validate()) {
8292
8978
  return { success: false, file_path, error: "Validation failed after removing component" };
8293
8979
  }
@@ -8306,9 +8992,6 @@ function addRemovedGameObject(options) {
8306
8992
  if (!import_fs10.existsSync(file_path)) {
8307
8993
  return { success: false, file_path, error: `File not found: ${file_path}` };
8308
8994
  }
8309
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
8310
- return { success: false, file_path, error: `Invalid GameObject reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
8311
- }
8312
8995
  let doc;
8313
8996
  try {
8314
8997
  doc = UnityDocument.from_file(file_path);
@@ -8319,19 +9002,16 @@ function addRemovedGameObject(options) {
8319
9002
  if (!targetBlock) {
8320
9003
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8321
9004
  }
8322
- let rawText = targetBlock.raw;
8323
- if (/m_RemovedGameObjects:\s*\[\]/.test(rawText)) {
8324
- rawText = rawText.replace(/m_RemovedGameObjects:\s*\[\]/, "m_RemovedGameObjects:");
8325
- }
8326
- const removedPattern = /m_RemovedGameObjects:\s*\n/;
8327
- if (removedPattern.test(rawText)) {
8328
- rawText = rawText.replace(removedPattern, `m_RemovedGameObjects:
8329
- - ${component_ref}
8330
- `);
8331
- } else {
8332
- return { success: false, file_path, error: "m_RemovedGameObjects property not found in PrefabInstance" };
9005
+ const mutation = mutatePrefabReferenceList({
9006
+ target_block: targetBlock,
9007
+ key: "m_RemovedGameObjects",
9008
+ ref_text: component_ref,
9009
+ action: "add",
9010
+ item_label: "GameObject"
9011
+ });
9012
+ if (!mutation.success) {
9013
+ return { success: false, file_path, error: mutation.error };
8333
9014
  }
8334
- targetBlock.replace_raw(rawText);
8335
9015
  if (!doc.validate()) {
8336
9016
  return { success: false, file_path, error: "Validation failed after adding removed GameObject" };
8337
9017
  }
@@ -8360,13 +9040,16 @@ function removeRemovedGameObject(options) {
8360
9040
  if (!targetBlock) {
8361
9041
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
8362
9042
  }
8363
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8364
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
8365
- if (!refPattern.test(targetBlock.raw)) {
8366
- return { success: false, file_path, error: `GameObject reference "${component_ref}" not found in m_RemovedGameObjects` };
9043
+ const mutation = mutatePrefabReferenceList({
9044
+ target_block: targetBlock,
9045
+ key: "m_RemovedGameObjects",
9046
+ ref_text: component_ref,
9047
+ action: "remove",
9048
+ item_label: "GameObject"
9049
+ });
9050
+ if (!mutation.success) {
9051
+ return { success: false, file_path, error: mutation.error };
8367
9052
  }
8368
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
8369
- targetBlock.replace_raw(updatedRaw);
8370
9053
  if (!doc.validate()) {
8371
9054
  return { success: false, file_path, error: "Validation failed after removing GameObject" };
8372
9055
  }
@@ -8853,6 +9536,7 @@ function search_project(options) {
8853
9536
  const files = walk_project_files(project_path, extensions);
8854
9537
  const scanner = new UnityScanner;
8855
9538
  const matches = [];
9539
+ let files_with_errors = 0;
8856
9540
  for (const file of files) {
8857
9541
  try {
8858
9542
  let gameObjects;
@@ -8939,6 +9623,7 @@ function search_project(options) {
8939
9623
  if (max_matches !== undefined && matches.length >= max_matches)
8940
9624
  break;
8941
9625
  } catch {
9626
+ files_with_errors++;
8942
9627
  continue;
8943
9628
  }
8944
9629
  }
@@ -8947,6 +9632,7 @@ function search_project(options) {
8947
9632
  project_path,
8948
9633
  total_files_scanned: files.length,
8949
9634
  total_matches: matches.length,
9635
+ files_with_errors: files_with_errors > 0 ? files_with_errors : undefined,
8950
9636
  cursor: 0,
8951
9637
  truncated: max_matches !== undefined && matches.length >= max_matches,
8952
9638
  matches
@@ -9220,7 +9906,8 @@ function removeComponent(options) {
9220
9906
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
9221
9907
  }
9222
9908
  const resolved_file_id = found.file_id;
9223
- const goMatch = found.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
9909
+ let removed_added_component_overrides = 0;
9910
+ const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9224
9911
  if (goMatch) {
9225
9912
  const parentGoId = goMatch[1];
9226
9913
  const goBlock = doc.find_by_file_id(parentGoId);
@@ -9228,7 +9915,29 @@ function removeComponent(options) {
9228
9915
  const escaped = resolved_file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9229
9916
  const compLinePattern = new RegExp(`^[ \\t]*- component: \\{fileID: ${escaped}\\}[ \\t]*\\n`, "m");
9230
9917
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
9918
+ if (modifiedRaw === goBlock.raw) {}
9231
9919
  goBlock.replace_raw(modifiedRaw);
9920
+ if (goBlock.is_stripped) {
9921
+ const piMatch = goBlock.raw.match(/m_PrefabInstance:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9922
+ if (piMatch) {
9923
+ const piBlock = doc.find_by_file_id(piMatch[1]);
9924
+ if (piBlock && piBlock.class_id === 1001) {
9925
+ const updated = removeAddedComponentOverrideEntries(piBlock.raw, resolved_file_id);
9926
+ if (updated.removed_count > 0) {
9927
+ piBlock.replace_raw(updated.updated_raw);
9928
+ removed_added_component_overrides += updated.removed_count;
9929
+ }
9930
+ }
9931
+ }
9932
+ }
9933
+ }
9934
+ }
9935
+ const dangling = [];
9936
+ for (const block of doc.blocks) {
9937
+ if (block.file_id === resolved_file_id)
9938
+ continue;
9939
+ if (block.raw.includes(`{fileID: ${resolved_file_id}}`)) {
9940
+ dangling.push(block.file_id);
9232
9941
  }
9233
9942
  }
9234
9943
  doc.remove_block(resolved_file_id);
@@ -9243,7 +9952,8 @@ function removeComponent(options) {
9243
9952
  success: true,
9244
9953
  file_path,
9245
9954
  removed_file_id: resolved_file_id,
9246
- removed_class_id: found.class_id
9955
+ removed_class_id: found.class_id,
9956
+ 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
9247
9957
  };
9248
9958
  }
9249
9959
  function removeComponentBatch(options) {
@@ -9304,7 +10014,7 @@ function deleteGameObject(options) {
9304
10014
  const goBlock = goResult;
9305
10015
  const goId = goBlock.file_id;
9306
10016
  const componentIds = new Set;
9307
- const compMatches = goBlock.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
10017
+ const compMatches = goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
9308
10018
  for (const cm of compMatches) {
9309
10019
  componentIds.add(cm[1]);
9310
10020
  }
@@ -9314,7 +10024,7 @@ function deleteGameObject(options) {
9314
10024
  const block = doc.find_by_file_id(compId);
9315
10025
  if (block && block.class_id === 4) {
9316
10026
  transformId = compId;
9317
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
10027
+ const fatherMatch = block.raw.match(/m_Father:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
9318
10028
  if (fatherMatch) {
9319
10029
  fatherId = fatherMatch[1];
9320
10030
  }
@@ -9361,6 +10071,124 @@ function findPrefabInstanceBlock2(doc, identifier) {
9361
10071
  }
9362
10072
  return null;
9363
10073
  }
10074
+ function splitPrefabArraySection3(rawText, key) {
10075
+ const lines = rawText.split(`
10076
+ `);
10077
+ const keyPattern = new RegExp(`^[ ]*${key}:[ ]*(.*)$`);
10078
+ let keyIndex = -1;
10079
+ let keyIndentLen = 0;
10080
+ for (let i = 0;i < lines.length; i++) {
10081
+ const match = lines[i].match(keyPattern);
10082
+ if (match) {
10083
+ keyIndex = i;
10084
+ keyIndentLen = (lines[i].match(/^[ \t]*/) || [""])[0].length;
10085
+ break;
10086
+ }
10087
+ }
10088
+ if (keyIndex === -1) {
10089
+ return null;
10090
+ }
10091
+ let sectionEnd = keyIndex + 1;
10092
+ for (let i = keyIndex + 1;i < lines.length; i++) {
10093
+ const line = lines[i];
10094
+ const trimmed = line.trim();
10095
+ if (trimmed.length === 0) {
10096
+ sectionEnd = i + 1;
10097
+ continue;
10098
+ }
10099
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
10100
+ if (indentLen < keyIndentLen) {
10101
+ break;
10102
+ }
10103
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
10104
+ break;
10105
+ }
10106
+ sectionEnd = i + 1;
10107
+ }
10108
+ return {
10109
+ lines,
10110
+ key_index: keyIndex,
10111
+ section_end: sectionEnd
10112
+ };
10113
+ }
10114
+ function collectAddedObjectFileIdsFromArray(rawText, key) {
10115
+ const section = splitPrefabArraySection3(rawText, key);
10116
+ if (!section)
10117
+ return [];
10118
+ const keyLine = section.lines[section.key_index].trim();
10119
+ if (new RegExp(`^${key}:[ ]*[]$`).test(keyLine)) {
10120
+ return [];
10121
+ }
10122
+ const sectionText = section.lines.slice(section.key_index, section.section_end).join(`
10123
+ `);
10124
+ const fileIds = new Set;
10125
+ const addedObjectPattern = /addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/g;
10126
+ let match;
10127
+ while ((match = addedObjectPattern.exec(sectionText)) !== null) {
10128
+ fileIds.add(match[1]);
10129
+ }
10130
+ if (fileIds.size === 0) {
10131
+ const inlineEntryPattern = /^[ \t]*-[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/gm;
10132
+ while ((match = inlineEntryPattern.exec(sectionText)) !== null) {
10133
+ fileIds.add(match[1]);
10134
+ }
10135
+ }
10136
+ return [...fileIds];
10137
+ }
10138
+ function removeAddedComponentOverrideEntries(rawText, componentId) {
10139
+ const section = splitPrefabArraySection3(rawText, "m_AddedComponents");
10140
+ if (!section)
10141
+ return { updated_raw: rawText, removed_count: 0 };
10142
+ const keyLine = section.lines[section.key_index].trim();
10143
+ if (/^m_AddedComponents:[ \t]*\[\]$/.test(keyLine)) {
10144
+ return { updated_raw: rawText, removed_count: 0 };
10145
+ }
10146
+ const bodyLines = section.lines.slice(section.key_index + 1, section.section_end);
10147
+ const keptEntries = [];
10148
+ let removed = 0;
10149
+ let i = 0;
10150
+ while (i < bodyLines.length) {
10151
+ const line = bodyLines[i];
10152
+ const trimmed = line.trimStart();
10153
+ if (!trimmed.startsWith("-")) {
10154
+ i++;
10155
+ continue;
10156
+ }
10157
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
10158
+ const entryLines = [line];
10159
+ i++;
10160
+ while (i < bodyLines.length) {
10161
+ const nextLine = bodyLines[i];
10162
+ const nextTrimmed = nextLine.trim();
10163
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
10164
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
10165
+ break;
10166
+ }
10167
+ entryLines.push(nextLine);
10168
+ i++;
10169
+ }
10170
+ const entryText = entryLines.join(`
10171
+ `);
10172
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
10173
+ if (addedObjectMatch && addedObjectMatch[1] === componentId) {
10174
+ removed++;
10175
+ continue;
10176
+ }
10177
+ keptEntries.push(entryLines);
10178
+ }
10179
+ if (removed === 0) {
10180
+ return { updated_raw: rawText, removed_count: 0 };
10181
+ }
10182
+ const keyIndent = (section.lines[section.key_index].match(/^[ \t]*/) || [""])[0];
10183
+ const replacement = keptEntries.length === 0 ? [`${keyIndent}m_AddedComponents: []`] : [`${keyIndent}m_AddedComponents:`, ...keptEntries.flat()];
10184
+ const updatedLines = [
10185
+ ...section.lines.slice(0, section.key_index),
10186
+ ...replacement,
10187
+ ...section.lines.slice(section.section_end)
10188
+ ];
10189
+ return { updated_raw: updatedLines.join(`
10190
+ `), removed_count: removed };
10191
+ }
9364
10192
  function deletePrefabInstance(options) {
9365
10193
  const { file_path, prefab_instance } = options;
9366
10194
  const pathError = validate_file_path(file_path, "write");
@@ -9382,44 +10210,31 @@ function deletePrefabInstance(options) {
9382
10210
  }
9383
10211
  const piId = piBlock.file_id;
9384
10212
  const allToRemove = new Set([piId]);
9385
- const piRefPattern = new RegExp(`m_PrefabInstance:\\s*\\{fileID:\\s*${piId}\\}`);
10213
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${piId}\\}`);
9386
10214
  for (const block of doc.blocks) {
9387
10215
  if (block.is_stripped && piRefPattern.test(block.raw)) {
9388
10216
  allToRemove.add(block.file_id);
9389
10217
  }
9390
10218
  }
9391
- const addedGOPattern = /m_AddedGameObjects:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
9392
- const addedGOMatch = piBlock.raw.match(addedGOPattern);
9393
- if (addedGOMatch) {
9394
- const addedGOSection = addedGOMatch[1];
9395
- const goMatches = addedGOSection.matchAll(/fileID:\s*(\d+)/g);
9396
- for (const match of goMatches) {
9397
- const goId = match[1];
9398
- allToRemove.add(goId);
9399
- const goBlock = doc.find_by_file_id(goId);
9400
- if (goBlock) {
9401
- const compMatch = goBlock.raw.match(/component:\s*\{fileID:\s*(\d+)\}/);
9402
- if (compMatch) {
9403
- const transformId = compMatch[1];
9404
- allToRemove.add(transformId);
9405
- const descendants = doc.collect_hierarchy(transformId);
9406
- for (const id of descendants) {
9407
- allToRemove.add(id);
9408
- }
10219
+ for (const goId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedGameObjects")) {
10220
+ allToRemove.add(goId);
10221
+ const goBlock = doc.find_by_file_id(goId);
10222
+ if (goBlock) {
10223
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
10224
+ if (compMatch) {
10225
+ const transformId = compMatch[1];
10226
+ allToRemove.add(transformId);
10227
+ const descendants = doc.collect_hierarchy(transformId);
10228
+ for (const id of descendants) {
10229
+ allToRemove.add(id);
9409
10230
  }
9410
10231
  }
9411
10232
  }
9412
10233
  }
9413
- const addedCompPattern = /m_AddedComponents:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
9414
- const addedCompMatch = piBlock.raw.match(addedCompPattern);
9415
- if (addedCompMatch) {
9416
- const addedCompSection = addedCompMatch[1];
9417
- const compMatches = addedCompSection.matchAll(/fileID:\s*(\d+)/g);
9418
- for (const match of compMatches) {
9419
- allToRemove.add(match[1]);
9420
- }
10234
+ for (const componentId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedComponents")) {
10235
+ allToRemove.add(componentId);
9421
10236
  }
9422
- const parentMatch = piBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(\d+)\}/);
10237
+ const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
9423
10238
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
9424
10239
  let strippedRootTransformId = null;
9425
10240
  for (const id of allToRemove) {
@@ -10427,10 +11242,11 @@ function build_create_command() {
10427
11242
  if (!result.success)
10428
11243
  process.exitCode = 1;
10429
11244
  });
10430
- 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) => {
11245
+ 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) => {
10431
11246
  try {
11247
+ const resolvedProjectPath = resolve_project_path(options.project);
10432
11248
  const position = options.index !== undefined ? parseInt(options.index, 10) : undefined;
10433
- const result = add_scene(project_path, scene_path, { position });
11249
+ const result = add_scene(resolvedProjectPath, scene_path, { position });
10434
11250
  console.log(JSON.stringify(result, null, 2));
10435
11251
  if (!result.success)
10436
11252
  process.exitCode = 1;
@@ -10538,9 +11354,10 @@ NativeFormatImporter:
10538
11354
  shader_guid
10539
11355
  }, null, 2));
10540
11356
  });
10541
- 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) => {
11357
+ 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) => {
10542
11358
  try {
10543
- const result = add_package(project_path, name, version);
11359
+ const resolvedProjectPath = resolve_project_path(options.project);
11360
+ const result = add_package(resolvedProjectPath, name, version);
10544
11361
  if ("error" in result) {
10545
11362
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
10546
11363
  process.exitCode = 1;
@@ -10844,6 +11661,116 @@ init_scanner();
10844
11661
  var import_fs18 = require("fs");
10845
11662
  var import_path8 = require("path");
10846
11663
  var import_os = require("os");
11664
+ function parse_unity_ref(ref_text) {
11665
+ const file_id_match = ref_text.match(/fileID:[ \t]*(-?\d+)/i);
11666
+ if (!file_id_match)
11667
+ return null;
11668
+ const guid_match = ref_text.match(/guid:[ \t]*([a-f0-9]{32})/i);
11669
+ const type_match = ref_text.match(/type:[ \t]*(-?\d+)/i);
11670
+ const parsed = { file_id: file_id_match[1] };
11671
+ if (guid_match)
11672
+ parsed.guid = guid_match[1].toLowerCase();
11673
+ if (type_match)
11674
+ parsed.type = parseInt(type_match[1], 10);
11675
+ return parsed;
11676
+ }
11677
+ function extract_prefab_list_section(raw, key) {
11678
+ const lines = raw.split(`
11679
+ `);
11680
+ const key_pattern = new RegExp(`^(\\s*)${key}:[ \\t]*(.*)$`);
11681
+ let key_index = -1;
11682
+ let key_indent = 0;
11683
+ for (let i = 0;i < lines.length; i++) {
11684
+ const match = lines[i].match(key_pattern);
11685
+ if (match) {
11686
+ key_index = i;
11687
+ key_indent = match[1].length;
11688
+ break;
11689
+ }
11690
+ }
11691
+ if (key_index === -1)
11692
+ return null;
11693
+ const section_lines = [lines[key_index]];
11694
+ for (let i = key_index + 1;i < lines.length; i++) {
11695
+ const line = lines[i];
11696
+ const trimmed = line.trim();
11697
+ if (trimmed.length === 0) {
11698
+ section_lines.push(line);
11699
+ continue;
11700
+ }
11701
+ const indent = (line.match(/^\s*/) || [""])[0].length;
11702
+ if (indent < key_indent)
11703
+ break;
11704
+ if (indent === key_indent && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
11705
+ break;
11706
+ }
11707
+ section_lines.push(line);
11708
+ }
11709
+ return { key_indent, section_lines };
11710
+ }
11711
+ function parse_removed_list(raw, key) {
11712
+ const section = extract_prefab_list_section(raw, key);
11713
+ if (!section)
11714
+ return [];
11715
+ const key_line = section.section_lines[0].trim();
11716
+ if (key_line.endsWith("[]"))
11717
+ return [];
11718
+ const refs = section.section_lines.join(`
11719
+ `).match(/\{[^}]*fileID:[^}]*\}/g) || [];
11720
+ const dedup = new Map;
11721
+ for (const ref of refs) {
11722
+ const parsed = parse_unity_ref(ref);
11723
+ if (!parsed)
11724
+ continue;
11725
+ const key_text = `${parsed.file_id}|${parsed.guid ?? ""}|${parsed.type ?? ""}`;
11726
+ if (!dedup.has(key_text))
11727
+ dedup.set(key_text, parsed);
11728
+ }
11729
+ return [...dedup.values()];
11730
+ }
11731
+ function parse_added_list(raw, key) {
11732
+ const section = extract_prefab_list_section(raw, key);
11733
+ if (!section)
11734
+ return [];
11735
+ const key_line = section.section_lines[0].trim();
11736
+ if (key_line.endsWith("[]"))
11737
+ return [];
11738
+ const lines = section.section_lines.slice(1);
11739
+ const entries = [];
11740
+ let i = 0;
11741
+ while (i < lines.length) {
11742
+ const line = lines[i];
11743
+ const trimmed = line.trimStart();
11744
+ if (!trimmed.startsWith("-")) {
11745
+ i++;
11746
+ continue;
11747
+ }
11748
+ const entry_indent = (line.match(/^\s*/) || [""])[0].length;
11749
+ const entry_lines = [line];
11750
+ i++;
11751
+ while (i < lines.length) {
11752
+ const next = lines[i];
11753
+ const next_trimmed = next.trim();
11754
+ const next_indent = (next.match(/^\s*/) || [""])[0].length;
11755
+ if (next_trimmed.length > 0 && next_indent === entry_indent && next_trimmed.startsWith("-")) {
11756
+ break;
11757
+ }
11758
+ entry_lines.push(next);
11759
+ i++;
11760
+ }
11761
+ const entry_text = entry_lines.join(`
11762
+ `);
11763
+ const target_match = entry_text.match(/targetCorrespondingSourceObject:[ \t]*(\{[^}]+\})/);
11764
+ const insert_match = entry_text.match(/insertIndex:[ \t]*(-?\d+)/);
11765
+ const added_match = entry_text.match(/addedObject:[ \t]*(\{[^}]+\})/);
11766
+ entries.push({
11767
+ target_corresponding_source_object: target_match ? parse_unity_ref(target_match[1]) : null,
11768
+ insert_index: insert_match ? parseInt(insert_match[1], 10) : null,
11769
+ added_object: added_match ? parse_unity_ref(added_match[1]) : null
11770
+ });
11771
+ }
11772
+ return entries;
11773
+ }
10847
11774
  function parse_material_yaml(content) {
10848
11775
  const lines = content.replace(/\r/g, "").split(`
10849
11776
  `);
@@ -11123,10 +12050,12 @@ function parse_log_line_timestamp(line) {
11123
12050
  }
11124
12051
  function parse_log_entries(lines) {
11125
12052
  const entries = [];
11126
- const error_re = /^(error|exception|Error|Exception|ERROR)/i;
11127
- const warning_re = /^(warning|Warning|WARNING)/i;
12053
+ const error_re = /\b(error|exception)\b/i;
12054
+ const runtime_error_re = /\b(NullReferenceException|IndexOutOfRangeException|ArgumentException|MissingReferenceException|StackOverflowException|DivideByZeroException|InvalidOperationException|KeyNotFoundException|FormatException|ObjectDisposedException|MissingComponentException|UnassignedReferenceException)\b/;
12055
+ const warning_re = /\bwarning\b/i;
11128
12056
  const compile_re = /Assets\/.*\.cs\(\d+,\d+\):\s*error\s+CS/;
11129
12057
  const import_error_re = /Failed to import|Error while importing|Could not create asset|Unable to import|Shader error in|Import of asset .* failed/i;
12058
+ const assertion_re = /^Assertion failed/;
11130
12059
  const stack_re = /^\s+at\s+|^\s*\(Filename:/;
11131
12060
  for (let i = 0;i < lines.length; i++) {
11132
12061
  const line = lines[i];
@@ -11135,7 +12064,9 @@ function parse_log_entries(lines) {
11135
12064
  let level = "info";
11136
12065
  if (import_error_re.test(line))
11137
12066
  level = "import_error";
11138
- else if (error_re.test(line) || compile_re.test(line))
12067
+ else if (compile_re.test(line))
12068
+ level = "error";
12069
+ else if (error_re.test(line) || runtime_error_re.test(line) || assertion_re.test(line))
11139
12070
  level = "error";
11140
12071
  else if (warning_re.test(line))
11141
12072
  level = "warning";
@@ -11543,9 +12474,16 @@ function build_read_command(getScanner) {
11543
12474
  }
11544
12475
  if (result.total === 0 && !result.error) {
11545
12476
  const prefabInstances = result.prefabInstances;
12477
+ let variantDetected = false;
12478
+ let variantResolvedFromSource = false;
11546
12479
  if (prefabInstances && prefabInstances.length > 0) {
12480
+ variantDetected = true;
11547
12481
  try {
11548
12482
  const doc = UnityDocument.from_file(file);
12483
+ const strippedGoCount = doc.blocks.filter((b) => b.class_id === 1 && b.is_stripped).length;
12484
+ if (strippedGoCount > 0) {
12485
+ variantDetected = true;
12486
+ }
11549
12487
  const projectPath = find_unity_project_root(import_path8.dirname(file));
11550
12488
  const resolved = resolve_source_prefab(doc, file, projectPath ?? undefined);
11551
12489
  if (resolved) {
@@ -11580,6 +12518,7 @@ function build_read_command(getScanner) {
11580
12518
  resultRecord.resolvedFromSource = true;
11581
12519
  resultRecord.sourcePrefab = resolved.source_path;
11582
12520
  resultRecord.sourceGuid = resolved.source_guid;
12521
+ variantResolvedFromSource = true;
11583
12522
  const variantNote = `PrefabVariant resolved from source prefab: ${resolved.source_path}`;
11584
12523
  result.warning = result.warning ? `${result.warning}; ${variantNote}` : variantNote;
11585
12524
  }
@@ -11590,8 +12529,13 @@ function build_read_command(getScanner) {
11590
12529
  try {
11591
12530
  const fileSize = import_fs18.statSync(file).size;
11592
12531
  if (fileSize > 100) {
11593
- const corruptWarning = "File has valid Unity YAML header but contains no parseable GameObjects -- file may be corrupt or malformed";
11594
- result.warning = result.warning ? `${result.warning}; ${corruptWarning}` : corruptWarning;
12532
+ if (variantDetected && !variantResolvedFromSource) {
12533
+ 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.";
12534
+ result.warning = result.warning ? `${result.warning}; ${variantWarning}` : variantWarning;
12535
+ } else {
12536
+ const corruptWarning = "File has valid Unity YAML header but contains no parseable GameObjects -- file may be corrupt or malformed";
12537
+ result.warning = result.warning ? `${result.warning}; ${corruptWarning}` : corruptWarning;
12538
+ }
11595
12539
  }
11596
12540
  } catch {}
11597
12541
  }
@@ -11864,38 +12808,41 @@ function build_read_command(getScanner) {
11864
12808
  by_type: options.unresolved ? undefined : byType
11865
12809
  };
11866
12810
  if (!cache) {
11867
- output._hint = "Run 'setup <project>' to resolve GUID paths";
12811
+ output._hint = "Run 'setup' (or 'setup -p <path>') to resolve GUID paths";
11868
12812
  }
11869
12813
  console.log(JSON.stringify(output, null, 2));
11870
12814
  });
11871
- 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) => {
12815
+ 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) => {
12816
+ const resolvedProjectPath = resolve_project_path(options.project);
11872
12817
  const result = read_settings({
11873
- project_path,
12818
+ project_path: resolvedProjectPath,
11874
12819
  setting: options.setting
11875
12820
  });
11876
12821
  console.log(JSON.stringify(result, null, 2));
11877
12822
  if (!result.success)
11878
12823
  process.exit(1);
11879
12824
  });
11880
- cmd.command("build <project_path>").description("Read build settings (scene list, build profiles)").option("-j, --json", "Output as JSON").action((project_path, _options) => {
12825
+ 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) => {
12826
+ const resolvedProjectPath = resolve_project_path(options.project);
11881
12827
  try {
11882
- const result = get_build_settings(project_path);
12828
+ const result = get_build_settings(resolvedProjectPath);
11883
12829
  console.log(JSON.stringify(result, null, 2));
11884
12830
  } catch (err) {
11885
12831
  console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
11886
12832
  process.exitCode = 1;
11887
12833
  }
11888
12834
  });
11889
- cmd.command("scenes <project_path>").description('Read build scenes (alias for "read build")').option("-j, --json", "Output as JSON").action((project_path, _options) => {
12835
+ 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) => {
12836
+ const resolvedProjectPath = resolve_project_path(options.project);
11890
12837
  try {
11891
- const result = get_build_settings(project_path);
12838
+ const result = get_build_settings(resolvedProjectPath);
11892
12839
  console.log(JSON.stringify(result, null, 2));
11893
12840
  } catch (err) {
11894
12841
  console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
11895
12842
  process.exitCode = 1;
11896
12843
  }
11897
12844
  });
11898
- 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) => {
12845
+ 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) => {
11899
12846
  try {
11900
12847
  const doc = UnityDocument.from_file(file);
11901
12848
  let block = null;
@@ -11958,6 +12905,10 @@ function build_read_command(getScanner) {
11958
12905
  i++;
11959
12906
  }
11960
12907
  }
12908
+ const removed_components = parse_removed_list(block.raw, "m_RemovedComponents");
12909
+ const removed_gameobjects = parse_removed_list(block.raw, "m_RemovedGameObjects");
12910
+ const added_gameobjects = parse_added_list(block.raw, "m_AddedGameObjects");
12911
+ const added_components = parse_added_list(block.raw, "m_AddedComponents");
11961
12912
  if (options.flat) {
11962
12913
  const flat = modifications.map((m) => ({
11963
12914
  property_path: m.property_path,
@@ -11966,7 +12917,14 @@ function build_read_command(getScanner) {
11966
12917
  }));
11967
12918
  console.log(JSON.stringify(flat, null, 2));
11968
12919
  } else {
11969
- console.log(JSON.stringify(modifications, null, 2));
12920
+ console.log(JSON.stringify({
12921
+ prefab_instance_id: block.file_id,
12922
+ modifications,
12923
+ removed_components,
12924
+ removed_gameobjects,
12925
+ added_gameobjects,
12926
+ added_components
12927
+ }, null, 2));
11970
12928
  }
11971
12929
  } catch (err) {
11972
12930
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2));
@@ -12226,9 +13184,7 @@ function build_read_command(getScanner) {
12226
13184
  types: displayed
12227
13185
  }, null, 2));
12228
13186
  });
12229
- 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) => {
12230
- if (projectPath && !options.project)
12231
- options.project = projectPath;
13187
+ 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) => {
12232
13188
  const logPath = options.path || get_editor_log_path();
12233
13189
  if (!logPath || !import_fs18.existsSync(logPath)) {
12234
13190
  console.log(JSON.stringify({ error: `Editor.log not found${logPath ? `: ${logPath}` : ". Could not detect platform log path."}` }, null, 2));
@@ -12237,9 +13193,9 @@ function build_read_command(getScanner) {
12237
13193
  const content = import_fs18.readFileSync(logPath, "utf-8");
12238
13194
  let lines = content.split(/\r?\n/);
12239
13195
  if (options.project) {
12240
- const projectPath2 = import_path8.resolve(options.project);
12241
- const projectName = import_path8.basename(projectPath2);
12242
- const escapedPath = projectPath2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13196
+ const projectPath = import_path8.resolve(options.project);
13197
+ const projectName = import_path8.basename(projectPath);
13198
+ const escapedPath = projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12243
13199
  const escapedName = projectName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12244
13200
  const session_markers = [
12245
13201
  new RegExp(`Loading project at '${escapedPath}'`),
@@ -12307,10 +13263,6 @@ function build_read_command(getScanner) {
12307
13263
  }
12308
13264
  }
12309
13265
  }
12310
- if (options.search) {
12311
- const re = new RegExp(options.search, "i");
12312
- lines = lines.filter((l) => re.test(l));
12313
- }
12314
13266
  if (options.errors || options.warnings || options.compileErrors || options.importErrors) {
12315
13267
  const entries = parse_log_entries(lines);
12316
13268
  let filtered = entries;
@@ -12324,6 +13276,10 @@ function build_read_command(getScanner) {
12324
13276
  } else if (options.warnings) {
12325
13277
  filtered = entries.filter((e) => e.level === "warning");
12326
13278
  }
13279
+ if (options.search) {
13280
+ const re = new RegExp(options.search, "i");
13281
+ filtered = filtered.filter((e) => re.test(e.message) || e.stack_trace && e.stack_trace.some((s) => re.test(s)));
13282
+ }
12327
13283
  const tail_filtered = parseInt(options.tail, 10);
12328
13284
  if (isNaN(tail_filtered) || tail_filtered < 1) {
12329
13285
  console.log(JSON.stringify({ error: `Invalid --tail value "${options.tail}". Must be a positive integer.` }, null, 2));
@@ -12339,6 +13295,10 @@ function build_read_command(getScanner) {
12339
13295
  }, null, 2));
12340
13296
  return;
12341
13297
  }
13298
+ if (options.search) {
13299
+ const re = new RegExp(options.search, "i");
13300
+ lines = lines.filter((l) => re.test(l));
13301
+ }
12342
13302
  const tail = parseInt(options.tail, 10);
12343
13303
  if (isNaN(tail) || tail < 1) {
12344
13304
  console.log(JSON.stringify({ error: `Invalid --tail value "${options.tail}". Must be a positive integer.` }, null, 2));
@@ -12680,7 +13640,7 @@ function build_read_command(getScanner) {
12680
13640
  }
12681
13641
  const states_out = { file, states_by_layer: by_layer };
12682
13642
  if (needs_setup_hint)
12683
- states_out._hint = "Run 'setup <project>' to resolve motion paths";
13643
+ states_out._hint = "Run 'setup' (or 'setup -p <path>') to resolve motion paths";
12684
13644
  console.log(JSON.stringify(states_out, null, 2));
12685
13645
  return;
12686
13646
  }
@@ -12729,14 +13689,15 @@ function build_read_command(getScanner) {
12729
13689
  }))
12730
13690
  };
12731
13691
  if (needs_setup_hint)
12732
- default_out._hint = "Run 'setup <project>' to resolve motion paths";
13692
+ default_out._hint = "Run 'setup' (or 'setup -p <path>') to resolve motion paths";
12733
13693
  console.log(JSON.stringify(default_out, null, 2));
12734
13694
  });
12735
- 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) => {
13695
+ 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) => {
12736
13696
  if (!/^[0-9a-f]{32}$/i.test(guid)) {
12737
13697
  console.log(JSON.stringify({ error: "GUID must be a 32-character hexadecimal string" }, null, 2));
12738
13698
  process.exit(1);
12739
13699
  }
13700
+ const project_path = resolve_project_path(options.project);
12740
13701
  const assetsDir = import_path8.join(import_path8.resolve(project_path), "Assets");
12741
13702
  if (!import_fs18.existsSync(assetsDir)) {
12742
13703
  console.log(JSON.stringify({ error: `Assets directory not found in "${project_path}"` }, null, 2));
@@ -12794,11 +13755,11 @@ function build_read_command(getScanner) {
12794
13755
  by_type
12795
13756
  }, null, 2));
12796
13757
  });
12797
- 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) => {
12798
- const resolvedProject = import_path8.resolve(project_path);
13758
+ 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) => {
13759
+ const resolvedProject = resolve_project_path(options.project);
12799
13760
  const assetsDir = import_path8.join(resolvedProject, "Assets");
12800
13761
  if (!import_fs18.existsSync(assetsDir)) {
12801
- console.log(JSON.stringify({ error: `Assets directory not found in "${project_path}"` }, null, 2));
13762
+ console.log(JSON.stringify({ error: `Assets directory not found in "${resolvedProject}"` }, null, 2));
12802
13763
  process.exit(1);
12803
13764
  }
12804
13765
  const guidCacheObj = load_guid_cache(resolvedProject);
@@ -12889,7 +13850,8 @@ function build_read_command(getScanner) {
12889
13850
  by_type
12890
13851
  }, null, 2));
12891
13852
  });
12892
- 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) => {
13853
+ 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) => {
13854
+ const project_path = resolve_project_path(options.project);
12893
13855
  const result = list_packages(project_path, options.search);
12894
13856
  if ("error" in result) {
12895
13857
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
@@ -13126,7 +14088,7 @@ function build_update_command(getScanner) {
13126
14088
  if (!result.success)
13127
14089
  process.exitCode = 1;
13128
14090
  });
13129
- 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").action((file, file_id, property, value, _options) => {
14091
+ 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) => {
13130
14092
  if (property === "m_RootOrder") {
13131
14093
  const num = Number(value);
13132
14094
  if (!Number.isInteger(num) || num < 0) {
@@ -13139,7 +14101,8 @@ function build_update_command(getScanner) {
13139
14101
  file_path: file,
13140
14102
  file_id,
13141
14103
  property,
13142
- new_value: value
14104
+ new_value: value,
14105
+ project_path: options.project
13143
14106
  });
13144
14107
  console.log(JSON.stringify(result, null, 2));
13145
14108
  if (!result.success)
@@ -13207,13 +14170,14 @@ function build_update_command(getScanner) {
13207
14170
  if (!result.success)
13208
14171
  process.exitCode = 1;
13209
14172
  });
13210
- 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) => {
14173
+ 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) => {
13211
14174
  if (!options.setting || !options.property || !options.value) {
13212
14175
  console.log(JSON.stringify({ success: false, error: "Required: --setting, --property, --value" }, null, 2));
13213
14176
  process.exit(1);
13214
14177
  }
14178
+ const resolvedProjectPath = resolve_project_path(options.project);
13215
14179
  const result = edit_settings({
13216
- project_path,
14180
+ project_path: resolvedProjectPath,
13217
14181
  setting: options.setting,
13218
14182
  property: options.property,
13219
14183
  value: options.value
@@ -13222,13 +14186,14 @@ function build_update_command(getScanner) {
13222
14186
  if (!result.success)
13223
14187
  process.exitCode = 1;
13224
14188
  });
13225
- 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) => {
14189
+ 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) => {
13226
14190
  if (action !== "add" && action !== "remove") {
13227
14191
  console.log(JSON.stringify({ success: false, error: 'Action must be "add" or "remove"' }, null, 2));
13228
14192
  process.exit(1);
13229
14193
  }
14194
+ const resolvedProjectPath = resolve_project_path(options.project);
13230
14195
  const result = edit_tag({
13231
- project_path,
14196
+ project_path: resolvedProjectPath,
13232
14197
  action,
13233
14198
  tag
13234
14199
  });
@@ -13236,9 +14201,10 @@ function build_update_command(getScanner) {
13236
14201
  if (!result.success)
13237
14202
  process.exitCode = 1;
13238
14203
  });
13239
- 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) => {
14204
+ 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) => {
14205
+ const resolvedProjectPath = resolve_project_path(options.project);
13240
14206
  const result = edit_layer({
13241
- project_path,
14207
+ project_path: resolvedProjectPath,
13242
14208
  index: parseInt(index, 10),
13243
14209
  name
13244
14210
  });
@@ -13246,13 +14212,14 @@ function build_update_command(getScanner) {
13246
14212
  if (!result.success)
13247
14213
  process.exitCode = 1;
13248
14214
  });
13249
- 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) => {
14215
+ 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) => {
13250
14216
  if (action !== "add" && action !== "remove") {
13251
14217
  console.log(JSON.stringify({ success: false, error: 'Action must be "add" or "remove"' }, null, 2));
13252
14218
  process.exit(1);
13253
14219
  }
14220
+ const resolvedProjectPath = resolve_project_path(options.project);
13254
14221
  const result = edit_sorting_layer({
13255
- project_path,
14222
+ project_path: resolvedProjectPath,
13256
14223
  action,
13257
14224
  name
13258
14225
  });
@@ -13352,20 +14319,21 @@ function build_update_command(getScanner) {
13352
14319
  if (!result.success)
13353
14320
  process.exitCode = 1;
13354
14321
  });
13355
- 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) => {
14322
+ 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) => {
13356
14323
  try {
14324
+ const resolvedProjectPath = resolve_project_path(options.project);
13357
14325
  if (options.move !== undefined) {
13358
- const result = move_scene(project_path, scene_path, parseInt(options.move, 10));
14326
+ const result = move_scene(resolvedProjectPath, scene_path, parseInt(options.move, 10));
13359
14327
  console.log(JSON.stringify(result, null, 2));
13360
14328
  if (!result.success)
13361
14329
  process.exitCode = 1;
13362
14330
  } else if (options.enable) {
13363
- const result = enable_scene(project_path, scene_path);
14331
+ const result = enable_scene(resolvedProjectPath, scene_path);
13364
14332
  console.log(JSON.stringify(result, null, 2));
13365
14333
  if (!result.success)
13366
14334
  process.exitCode = 1;
13367
14335
  } else if (options.disable) {
13368
- const result = disable_scene(project_path, scene_path);
14336
+ const result = disable_scene(resolvedProjectPath, scene_path);
13369
14337
  console.log(JSON.stringify(result, null, 2));
13370
14338
  if (!result.success)
13371
14339
  process.exitCode = 1;
@@ -15059,9 +16027,10 @@ function build_delete_command() {
15059
16027
  process.exitCode = 1;
15060
16028
  }
15061
16029
  });
15062
- 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) => {
16030
+ 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) => {
16031
+ const resolvedProjectPath = resolve_project_path(options.project);
15063
16032
  try {
15064
- const result = remove_scene(project_path, scene_path);
16033
+ const result = remove_scene(resolvedProjectPath, scene_path);
15065
16034
  console.log(JSON.stringify(result, null, 2));
15066
16035
  if (!result.success)
15067
16036
  process.exitCode = 1;
@@ -15079,9 +16048,10 @@ function build_delete_command() {
15079
16048
  if (!result.success)
15080
16049
  process.exitCode = 1;
15081
16050
  });
15082
- 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) => {
16051
+ 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) => {
15083
16052
  try {
15084
- const result = remove_package(project_path, name);
16053
+ const resolvedProjectPath = resolve_project_path(options.project);
16054
+ const result = remove_package(resolvedProjectPath, name);
15085
16055
  if ("error" in result) {
15086
16056
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15087
16057
  process.exitCode = 1;
@@ -15322,6 +16292,30 @@ async function stream_editor(options) {
15322
16292
  connect(`ws://127.0.0.1:${config.port}/unity-agentic`);
15323
16293
  });
15324
16294
  }
16295
+ async function ping_editor(port, timeout_ms = 2000) {
16296
+ return new Promise((resolve7) => {
16297
+ const timer = setTimeout(() => {
16298
+ resolve7({ reachable: false, error: `Timeout after ${timeout_ms}ms` });
16299
+ }, timeout_ms);
16300
+ try {
16301
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/unity-agentic`);
16302
+ ws.onopen = () => {
16303
+ clearTimeout(timer);
16304
+ try {
16305
+ ws.close();
16306
+ } catch {}
16307
+ resolve7({ reachable: true });
16308
+ };
16309
+ ws.onerror = (err) => {
16310
+ clearTimeout(timer);
16311
+ resolve7({ reachable: false, error: String(err) });
16312
+ };
16313
+ } catch (err) {
16314
+ clearTimeout(timer);
16315
+ resolve7({ reachable: false, error: String(err) });
16316
+ }
16317
+ });
16318
+ }
15325
16319
  function resolve_config(options) {
15326
16320
  if (options.port) {
15327
16321
  return { port: options.port, pid: 0, version: "unknown" };
@@ -15344,7 +16338,7 @@ function generate_id() {
15344
16338
  var BRIDGE_PACKAGE_NAME = "com.unity-agentic-tools.editor-bridge";
15345
16339
  var BRIDGE_PACKAGE_VERSION = "https://github.com/taconotsandwich/unity-agentic-tools.git?path=unity-package";
15346
16340
  function get_common_options(cmd) {
15347
- let current = cmd.parent;
16341
+ let current = cmd;
15348
16342
  let project;
15349
16343
  let timeout_str;
15350
16344
  let port_str;
@@ -15394,11 +16388,14 @@ function build_editor_command() {
15394
16388
  process.exitCode = 1;
15395
16389
  return;
15396
16390
  }
16391
+ const ping = await ping_editor(config.port, 2000);
15397
16392
  console.log(JSON.stringify({
15398
16393
  port: config.port,
15399
16394
  pid: config.pid,
15400
16395
  version: config.version,
15401
- project_path
16396
+ project_path,
16397
+ bridge_reachable: ping.reachable,
16398
+ ...ping.reachable ? {} : { bridge_error: ping.error }
15402
16399
  }, null, 2));
15403
16400
  });
15404
16401
  cmd.command("play").description("Enter play mode").action(async function() {
@@ -15533,9 +16530,9 @@ function build_editor_command() {
15533
16530
  params.mode = options.mode;
15534
16531
  await handle_rpc(this, "editor.tests.run", params);
15535
16532
  });
15536
- cmd.command("install <project_path>").description("Install the editor bridge package into a Unity project").action((_project_path) => {
15537
- const resolved_path = import_path11.resolve(_project_path);
15538
- const result = add_package(resolved_path, BRIDGE_PACKAGE_NAME, BRIDGE_PACKAGE_VERSION);
16533
+ 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() {
16534
+ const { project_path } = get_common_options(this);
16535
+ const result = add_package(project_path, BRIDGE_PACKAGE_NAME, BRIDGE_PACKAGE_VERSION);
15539
16536
  if ("error" in result) {
15540
16537
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15541
16538
  process.exitCode = 1;
@@ -15543,9 +16540,9 @@ function build_editor_command() {
15543
16540
  }
15544
16541
  console.log(JSON.stringify(result, null, 2));
15545
16542
  });
15546
- cmd.command("uninstall <project_path>").description("Remove the editor bridge package from a Unity project").action((_project_path) => {
15547
- const resolved_path = import_path11.resolve(_project_path);
15548
- const result = remove_package(resolved_path, BRIDGE_PACKAGE_NAME);
16543
+ 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() {
16544
+ const { project_path } = get_common_options(this);
16545
+ const result = remove_package(project_path, BRIDGE_PACKAGE_NAME);
15549
16546
  if ("error" in result) {
15550
16547
  console.log(JSON.stringify({ success: false, error: result.error }, null, 2));
15551
16548
  process.exitCode = 1;
@@ -15876,11 +16873,12 @@ program.command("search <path> [pattern]").description("Search for GameObjects.
15876
16873
  console.log(JSON.stringify(result, null, 2));
15877
16874
  }
15878
16875
  });
15879
- 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) => {
16876
+ 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) => {
15880
16877
  if (!pattern || pattern.trim() === "") {
15881
16878
  console.log(JSON.stringify({ success: false, error: "Pattern must not be empty" }, null, 2));
15882
16879
  process.exit(1);
15883
16880
  }
16881
+ let project_path = resolve_project_path(options.project);
15884
16882
  const abs_path = path9.resolve(project_path);
15885
16883
  const project_root = find_unity_project_root(abs_path);
15886
16884
  if (project_root) {
@@ -15908,8 +16906,9 @@ program.command("grep <project_path> <pattern>").description("Search for a regex
15908
16906
  if (!result.success)
15909
16907
  process.exitCode = 1;
15910
16908
  });
15911
- program.command("version <project_path>").description("Read Unity project version").option("-j, --json", "Output as JSON").action((project_path, _options) => {
16909
+ 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) => {
15912
16910
  try {
16911
+ const project_path = resolve_project_path(options.project);
15913
16912
  const version = read_project_version(project_path);
15914
16913
  console.log(JSON.stringify(version, null, 2));
15915
16914
  } catch (err) {