@mflrevan/ucp 0.4.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,15 @@ namespace UCP.Bridge.Tests
21
21
  private const string TempTextPath = "Assets/UcpControllerSmoke.txt";
22
22
  private const string TempScriptPath = "Assets/UcpControllerSmokeComponent.cs";
23
23
  private const string TempTexturePath = "Assets/UcpImporterSmoke.png";
24
+ private const string TempScenePath = "Assets/UcpControllerMoveScene.unity";
25
+ private const string TempSceneBPath = "Assets/UcpControllerSceneB.unity";
26
+ private const string TempMovedFolderPath = "Assets/UcpControllerMoved";
27
+ private const string TempMovedAssetPath = "Assets/UcpControllerMoved/UcpControllerSmoke.asset";
28
+ private const string TempMovedReferenceAssetPath = "Assets/UcpControllerMoved/UcpControllerReference.asset";
29
+ private const string TempMovedMaterialPath = "Assets/UcpControllerMoved/UcpControllerSmoke.mat";
30
+ private const string TempMovedScenePath = "Assets/UcpControllerMoved/UcpControllerMoveScene.unity";
31
+ private const string TempReimportFolderPath = "Assets/UcpRecursiveReimport";
32
+ private const string TempReimportTexturePath = "Assets/UcpRecursiveReimport/UcpRecursiveA.png";
24
33
  private const string TempProfilerExportPath = "ProfilerCaptures\\smoke-export.json";
25
34
  private const string TempLocalPackageFolder = "TempUcpLocalPackage";
26
35
  private const string TempLocalPackageName = "com.ucp.temp.local";
@@ -34,6 +43,8 @@ namespace UCP.Bridge.Tests
34
43
  SnapshotController.Register(_router);
35
44
  AssetController.Register(_router);
36
45
  ImporterController.Register(_router);
46
+ PlayModeController.Register(_router);
47
+ ReferenceController.Register(_router);
37
48
  LogsController.Register(_router);
38
49
  HierarchyController.Register(_router);
39
50
  ProfilerController.Register(_router);
@@ -53,6 +64,10 @@ namespace UCP.Bridge.Tests
53
64
  DeleteTempTextFile();
54
65
  DeleteTempScriptFile();
55
66
  DeleteTempTextureAsset();
67
+ DeleteTempScene();
68
+ DeleteTempSceneB();
69
+ DeleteTempMovedFolder();
70
+ DeleteTempReimportFolder();
56
71
  DeleteTempProfilerExport();
57
72
  DeleteTempLocalPackage();
58
73
  RemoveTempLocalPackageDependencyIfPresent();
@@ -74,6 +89,10 @@ namespace UCP.Bridge.Tests
74
89
  DeleteTempTextFile();
75
90
  DeleteTempScriptFile();
76
91
  DeleteTempTextureAsset();
92
+ DeleteTempScene();
93
+ DeleteTempSceneB();
94
+ DeleteTempMovedFolder();
95
+ DeleteTempReimportFolder();
77
96
  DeleteTempProfilerExport();
78
97
  DeleteTempLocalPackage();
79
98
  RemoveTempLocalPackageDependencyIfPresent();
@@ -236,6 +255,64 @@ namespace UCP.Bridge.Tests
236
255
  Assert.That(Convert.ToBoolean(match["isSubAsset"]), Is.True);
237
256
  }
238
257
 
258
+ [Test]
259
+ public void AssetSearch_SupportsRegexNameFiltering()
260
+ {
261
+ const string RegexAssetPath = "Assets/SCN_101.asset";
262
+
263
+ try
264
+ {
265
+ var sceneAsset = ScriptableObject.CreateInstance<SearchRootAsset>();
266
+ sceneAsset.name = "SCN_101";
267
+ AssetDatabase.CreateAsset(sceneAsset, RegexAssetPath);
268
+ AssetDatabase.SaveAssets();
269
+
270
+ var response = _router.Dispatch(
271
+ "asset/search",
272
+ 1,
273
+ "{\"name\":\"^SCN_[0-9]+$\",\"regex\":true,\"path\":\"Assets\",\"maxResults\":10}");
274
+
275
+ Assert.That(response.error, Is.Null);
276
+
277
+ var result = (Dictionary<string, object>)response.result;
278
+ var matches = (List<object>)result["results"];
279
+ var match = FindAssetMatchByPath(matches, RegexAssetPath);
280
+ Assert.That(match, Is.Not.Null);
281
+ }
282
+ finally
283
+ {
284
+ if (AssetDatabase.AssetPathExists(RegexAssetPath))
285
+ {
286
+ AssetDatabase.DeleteAsset(RegexAssetPath);
287
+ AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
288
+ }
289
+ }
290
+ }
291
+
292
+ [Test]
293
+ public void AssetSearch_DoesNotEmitSceneReadObjectThreadedErrors()
294
+ {
295
+ EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
296
+ EditorSceneManager.SaveScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene(), TempScenePath);
297
+ AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
298
+ LogsController.ClearHistoryForTests();
299
+
300
+ var response = _router.Dispatch(
301
+ "asset/search",
302
+ 1,
303
+ "{\"name\":\"UcpControllerMoveScene\",\"path\":\"Assets\",\"maxResults\":10}");
304
+
305
+ Assert.That(response.error, Is.Null);
306
+
307
+ var status = _router.Dispatch("logs/status", 1, "{}");
308
+ Assert.That(status.error, Is.Null);
309
+
310
+ var result = (Dictionary<string, object>)status.result;
311
+ var byLevel = (Dictionary<string, object>)result["byLevel"];
312
+ Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(0));
313
+ Assert.That(Convert.ToInt32(byLevel["exception"]), Is.EqualTo(0));
314
+ }
315
+
239
316
  [Test]
240
317
  public void LogsTail_ReturnsRequestedBufferedCount()
241
318
  {
@@ -334,6 +411,30 @@ namespace UCP.Bridge.Tests
334
411
  Assert.That(result["stackTrace"], Is.EqualTo("stack line 1\nstack line 2"));
335
412
  }
336
413
 
414
+ [Test]
415
+ public void LogsStatus_CanScopeToNewEntriesAfterCursor()
416
+ {
417
+ var first = LogsController.RecordTestLog("info", "Before");
418
+ LogsController.RecordTestLog("warning", "After warning");
419
+ LogsController.RecordTestLog("error", "After error");
420
+
421
+ var response = _router.Dispatch(
422
+ "logs/status",
423
+ 1,
424
+ "{\"afterId\":" + Convert.ToInt64(first["id"]) + "}");
425
+
426
+ Assert.That(response.error, Is.Null);
427
+
428
+ var result = (Dictionary<string, object>)response.result;
429
+ Assert.That(Convert.ToInt64(result["afterId"]), Is.EqualTo(Convert.ToInt64(first["id"])));
430
+ Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(2));
431
+
432
+ var byLevel = (Dictionary<string, object>)result["byLevel"];
433
+ Assert.That(Convert.ToInt32(byLevel["info"]), Is.EqualTo(0));
434
+ Assert.That(Convert.ToInt32(byLevel["warning"]), Is.EqualTo(1));
435
+ Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(1));
436
+ }
437
+
337
438
  [Test]
338
439
  public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd()
339
440
  {
@@ -477,6 +578,166 @@ namespace UCP.Bridge.Tests
477
578
  Assert.That(AssetDatabase.GetAssetPath(reloaded.referenceAsset), Is.EqualTo(TempReferenceAssetPath));
478
579
  }
479
580
 
581
+ [Test]
582
+ public void AssetMove_PreservesCustomAssetReferenceAndGuid()
583
+ {
584
+ var reference = ScriptableObject.CreateInstance<SearchRootAsset>();
585
+ reference.name = "MoveTarget";
586
+ AssetDatabase.CreateAsset(reference, TempReferenceAssetPath);
587
+
588
+ var asset = ScriptableObject.CreateInstance<BatchWritableAsset>();
589
+ asset.referenceAsset = reference;
590
+ AssetDatabase.CreateAsset(asset, TempAssetPath);
591
+ AssetDatabase.SaveAssets();
592
+
593
+ var originalGuid = AssetDatabase.AssetPathToGUID(TempReferenceAssetPath);
594
+
595
+ var response = _router.Dispatch(
596
+ "asset/move",
597
+ 1,
598
+ "{\"path\":\"" + TempReferenceAssetPath + "\",\"destination\":\"" + TempMovedReferenceAssetPath + "\"}");
599
+
600
+ Assert.That(response.error, Is.Null);
601
+
602
+ var result = (Dictionary<string, object>)response.result;
603
+ Assert.That(Convert.ToBoolean(result["changed"]), Is.True);
604
+ Assert.That(result["sourcePath"], Is.EqualTo(TempReferenceAssetPath));
605
+ Assert.That(result["destinationPath"], Is.EqualTo(TempMovedReferenceAssetPath));
606
+ Assert.That(result["guid"], Is.EqualTo(originalGuid));
607
+
608
+ var reloaded = AssetDatabase.LoadAssetAtPath<BatchWritableAsset>(TempAssetPath);
609
+ Assert.That(reloaded, Is.Not.Null);
610
+ Assert.That(reloaded.referenceAsset, Is.Not.Null);
611
+ Assert.That(AssetDatabase.GetAssetPath(reloaded.referenceAsset), Is.EqualTo(TempMovedReferenceAssetPath));
612
+ Assert.That(AssetDatabase.AssetPathToGUID(TempMovedReferenceAssetPath), Is.EqualTo(originalGuid));
613
+ }
614
+
615
+ [Test]
616
+ public void AssetBulkMove_MovesMultipleAssetsAndPreservesReferences()
617
+ {
618
+ var reference = ScriptableObject.CreateInstance<SearchRootAsset>();
619
+ reference.name = "BulkMoveRef";
620
+ AssetDatabase.CreateAsset(reference, TempReferenceAssetPath);
621
+
622
+ var asset = ScriptableObject.CreateInstance<BatchWritableAsset>();
623
+ asset.referenceAsset = reference;
624
+ asset.maxPlayers = 7;
625
+ AssetDatabase.CreateAsset(asset, TempAssetPath);
626
+ AssetDatabase.SaveAssets();
627
+
628
+ var response = _router.Dispatch(
629
+ "asset/bulk-move",
630
+ 1,
631
+ "{\"moves\":["
632
+ + "{\"from\":\"" + TempReferenceAssetPath + "\",\"to\":\"" + TempMovedReferenceAssetPath + "\"},"
633
+ + "{\"from\":\"" + TempAssetPath + "\",\"to\":\"" + TempMovedAssetPath + "\"}"
634
+ + "]}");
635
+
636
+ Assert.That(response.error, Is.Null);
637
+
638
+ var result = (Dictionary<string, object>)response.result;
639
+ Assert.That(Convert.ToInt32(result["requested"]), Is.EqualTo(2));
640
+ Assert.That(Convert.ToInt32(result["moved"]), Is.EqualTo(2));
641
+ Assert.That(Convert.ToInt32(result["failed"]), Is.EqualTo(0));
642
+
643
+ var movedAsset = AssetDatabase.LoadAssetAtPath<BatchWritableAsset>(TempMovedAssetPath);
644
+ Assert.That(movedAsset, Is.Not.Null);
645
+ Assert.That(movedAsset.maxPlayers, Is.EqualTo(7));
646
+ Assert.That(movedAsset.referenceAsset, Is.Not.Null);
647
+ Assert.That(AssetDatabase.GetAssetPath(movedAsset.referenceAsset), Is.EqualTo(TempMovedReferenceAssetPath));
648
+ }
649
+
650
+ [Test]
651
+ public void AssetBulkMove_DryRunDoesNotMutateProject()
652
+ {
653
+ var asset = ScriptableObject.CreateInstance<SearchRootAsset>();
654
+ asset.name = "DryRunAsset";
655
+ AssetDatabase.CreateAsset(asset, TempAssetPath);
656
+ AssetDatabase.SaveAssets();
657
+
658
+ var response = _router.Dispatch(
659
+ "asset/bulk-move",
660
+ 1,
661
+ "{\"dryRun\":true,\"moves\":[{\"from\":\"" + TempAssetPath + "\",\"to\":\"" + TempMovedAssetPath + "\"}]}");
662
+
663
+ Assert.That(response.error, Is.Null);
664
+
665
+ var result = (Dictionary<string, object>)response.result;
666
+ Assert.That(Convert.ToBoolean(result["dryRun"]), Is.True);
667
+ Assert.That(Convert.ToInt32(result["moved"]), Is.EqualTo(1));
668
+ Assert.That(AssetDatabase.LoadAssetAtPath<SearchRootAsset>(TempAssetPath), Is.Not.Null);
669
+ Assert.That(AssetDatabase.LoadAssetAtPath<SearchRootAsset>(TempMovedAssetPath), Is.Null);
670
+ }
671
+
672
+ [Test]
673
+ public void AssetMove_PreservesSceneMaterialReference()
674
+ {
675
+ var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
676
+ Assert.That(shader, Is.Not.Null);
677
+
678
+ var material = new Material(shader) { name = "MoveSceneMaterial" };
679
+ AssetDatabase.CreateAsset(material, TempMaterialPath);
680
+ AssetDatabase.SaveAssets();
681
+
682
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
683
+ var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
684
+ cube.name = "MoveSceneCube";
685
+ cube.GetComponent<MeshRenderer>().sharedMaterial = material;
686
+ Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
687
+
688
+ var response = _router.Dispatch(
689
+ "asset/move",
690
+ 1,
691
+ "{\"path\":\"" + TempMaterialPath + "\",\"destination\":\"" + TempMovedMaterialPath + "\"}");
692
+
693
+ Assert.That(response.error, Is.Null);
694
+
695
+ EditorSceneManager.OpenScene(TempScenePath, OpenSceneMode.Single);
696
+ var movedCube = GameObject.Find("MoveSceneCube");
697
+ Assert.That(movedCube, Is.Not.Null);
698
+ var renderer = movedCube.GetComponent<MeshRenderer>();
699
+ Assert.That(renderer, Is.Not.Null);
700
+ Assert.That(renderer.sharedMaterial, Is.Not.Null);
701
+ Assert.That(AssetDatabase.GetAssetPath(renderer.sharedMaterial), Is.EqualTo(TempMovedMaterialPath));
702
+ }
703
+
704
+ [Test]
705
+ public void AssetMove_UpdatesBuildSettingsWhenSceneMoves()
706
+ {
707
+ var originalScenes = EditorBuildSettings.scenes;
708
+ try
709
+ {
710
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
711
+ var marker = new GameObject("MovedSceneMarker");
712
+ marker.transform.position = new Vector3(1f, 2f, 3f);
713
+ Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
714
+
715
+ var originalGuid = AssetDatabase.AssetPathToGUID(TempScenePath);
716
+ EditorBuildSettings.scenes = new[]
717
+ {
718
+ new EditorBuildSettingsScene(TempScenePath, true)
719
+ };
720
+
721
+ var response = _router.Dispatch(
722
+ "asset/move",
723
+ 1,
724
+ "{\"path\":\"" + TempScenePath + "\",\"destination\":\"" + TempMovedScenePath + "\"}");
725
+
726
+ Assert.That(response.error, Is.Null);
727
+ Assert.That(AssetDatabase.AssetPathToGUID(TempMovedScenePath), Is.EqualTo(originalGuid));
728
+
729
+ Assert.That(EditorBuildSettings.scenes, Has.Length.EqualTo(1));
730
+ Assert.That(EditorBuildSettings.scenes[0].path, Is.EqualTo(TempMovedScenePath));
731
+
732
+ EditorSceneManager.OpenScene(TempMovedScenePath, OpenSceneMode.Single);
733
+ Assert.That(GameObject.Find("MovedSceneMarker"), Is.Not.Null);
734
+ }
735
+ finally
736
+ {
737
+ EditorBuildSettings.scenes = originalScenes;
738
+ }
739
+ }
740
+
480
741
  [Test]
481
742
  public void FileController_WritePatchRead_AndRejectsPathTraversal()
482
743
  {
@@ -610,6 +871,53 @@ namespace UCP.Bridge.Tests
610
871
  Assert.That(importer.isReadable, Is.True);
611
872
  }
612
873
 
874
+ [Test]
875
+ public void ImporterController_ReimportRecursive_ReimportsFolderContents()
876
+ {
877
+ CreateTempRecursiveTextureAsset(Color.magenta);
878
+ AssetImportSupport.ClearTestState();
879
+
880
+ var response = _router.Dispatch(
881
+ "asset/reimport",
882
+ 1,
883
+ "{\"path\":\"" + TempReimportFolderPath + "\",\"recursive\":true}");
884
+
885
+ Assert.That(response.error, Is.Null);
886
+
887
+ var result = (Dictionary<string, object>)response.result;
888
+ Assert.That(Convert.ToBoolean(result["recursive"]), Is.True);
889
+ Assert.That(Convert.ToInt32(result["requested"]), Is.GreaterThanOrEqualTo(1));
890
+ Assert.That(Convert.ToInt32(result["reimported"]), Is.GreaterThanOrEqualTo(1));
891
+ Assert.That(AssetImportSupport.LastReimportedPathForTests, Is.EqualTo(TempReimportTexturePath));
892
+ }
893
+
894
+ [Test]
895
+ public void SceneController_Load_AdditiveKeepsExistingSceneLoaded()
896
+ {
897
+ var firstScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
898
+ new GameObject("SceneAObject");
899
+ Assert.That(EditorSceneManager.SaveScene(firstScene, TempScenePath), Is.True);
900
+
901
+ var secondScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
902
+ new GameObject("SceneBObject");
903
+ Assert.That(EditorSceneManager.SaveScene(secondScene, TempSceneBPath), Is.True);
904
+
905
+ EditorSceneManager.OpenScene(TempScenePath, OpenSceneMode.Single);
906
+
907
+ var response = _router.Dispatch(
908
+ "scene/load",
909
+ 1,
910
+ "{\"path\":\"" + TempSceneBPath + "\",\"additive\":true,\"saveDirtyScenes\":true,\"discardUntitled\":true}");
911
+
912
+ Assert.That(response.error, Is.Null);
913
+
914
+ var result = (Dictionary<string, object>)response.result;
915
+ Assert.That(Convert.ToBoolean(result["additive"]), Is.True);
916
+ Assert.That(UnityEngine.SceneManagement.SceneManager.sceneCount, Is.EqualTo(2));
917
+ Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempScenePath).isLoaded, Is.True);
918
+ Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempSceneBPath).isLoaded, Is.True);
919
+ }
920
+
613
921
  [Test]
614
922
  public void PackagesController_DependencySetInfoAndRemove_LocalFilePackage()
615
923
  {
@@ -935,6 +1243,42 @@ namespace UCP.Bridge.Tests
935
1243
  AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
936
1244
  }
937
1245
 
1246
+ private static void DeleteTempScene()
1247
+ {
1248
+ if (AssetDatabase.LoadMainAssetAtPath(TempScenePath) != null)
1249
+ {
1250
+ AssetDatabase.DeleteAsset(TempScenePath);
1251
+ AssetDatabase.SaveAssets();
1252
+ }
1253
+ }
1254
+
1255
+ private static void DeleteTempSceneB()
1256
+ {
1257
+ if (AssetDatabase.LoadMainAssetAtPath(TempSceneBPath) != null)
1258
+ {
1259
+ AssetDatabase.DeleteAsset(TempSceneBPath);
1260
+ AssetDatabase.SaveAssets();
1261
+ }
1262
+ }
1263
+
1264
+ private static void DeleteTempMovedFolder()
1265
+ {
1266
+ if (AssetDatabase.IsValidFolder(TempMovedFolderPath))
1267
+ {
1268
+ AssetDatabase.DeleteAsset(TempMovedFolderPath);
1269
+ AssetDatabase.SaveAssets();
1270
+ }
1271
+ }
1272
+
1273
+ private static void DeleteTempReimportFolder()
1274
+ {
1275
+ if (AssetDatabase.IsValidFolder(TempReimportFolderPath))
1276
+ {
1277
+ AssetDatabase.DeleteAsset(TempReimportFolderPath);
1278
+ AssetDatabase.SaveAssets();
1279
+ }
1280
+ }
1281
+
938
1282
  private static void DeleteTempProfilerExport()
939
1283
  {
940
1284
  var fullPath = ResolveProjectRelativePath(TempProfilerExportPath);
@@ -1014,6 +1358,24 @@ namespace UCP.Bridge.Tests
1014
1358
  ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
1015
1359
  }
1016
1360
 
1361
+ private static void CreateTempRecursiveTextureAsset(Color color)
1362
+ {
1363
+ DeleteTempReimportFolder();
1364
+ Directory.CreateDirectory(ResolveProjectRelativePath(TempReimportFolderPath));
1365
+
1366
+ var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
1367
+ texture.SetPixels(new[] { color, color, color, color });
1368
+ texture.Apply();
1369
+
1370
+ var bytes = texture.EncodeToPNG();
1371
+ UnityEngine.Object.DestroyImmediate(texture);
1372
+
1373
+ File.WriteAllBytes(ResolveProjectRelativePath(TempReimportTexturePath), bytes);
1374
+ AssetDatabase.ImportAsset(
1375
+ TempReimportTexturePath,
1376
+ ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
1377
+ }
1378
+
1017
1379
  private static string ResolveProjectRelativePath(string assetPath)
1018
1380
  {
1019
1381
  var projectRoot = Path.GetDirectoryName(Application.dataPath);
@@ -1062,6 +1424,19 @@ namespace UCP.Bridge.Tests
1062
1424
  return null;
1063
1425
  }
1064
1426
 
1427
+ private static Dictionary<string, object> FindAssetMatchByPath(List<object> matches, string expectedPath)
1428
+ {
1429
+ foreach (var entry in matches)
1430
+ {
1431
+ var match = (Dictionary<string, object>)entry;
1432
+ var path = match.ContainsKey("path") ? match["path"].ToString() : string.Empty;
1433
+ if (path == expectedPath)
1434
+ return match;
1435
+ }
1436
+
1437
+ return null;
1438
+ }
1439
+
1065
1440
  private sealed class SearchRootAsset : ScriptableObject
1066
1441
  {
1067
1442
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.ucp.bridge",
3
- "version": "0.4.6",
3
+ "version": "0.5.1",
4
4
  "displayName": "Unity Control Protocol Bridge",
5
5
  "description": "WebSocket bridge for programmatic Unity Editor control via CLI and AI agents.",
6
6
  "unity": "2021.3",
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@mflrevan/ucp",
3
- "version": "0.4.6",
3
+ "version": "0.5.1",
4
4
  "description": "Unity Control Protocol - CLI for programmatic Unity Editor control",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/mflRevan/unity-control-protocol.git"
8
+ "url": "git+https://github.com/mflRevan/unity-control-protocol.git"
9
9
  },
10
10
  "bin": {
11
11
  "ucp": "bin/ucp.js"