com.elestrago.unity.package-tools 2.0.11 → 2.2.0
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/CAHNGELOG.md +17 -0
- package/Documentation~/api.md +502 -0
- package/Documentation~/manual.md +140 -0
- package/Documentation~/samples.md +73 -0
- package/Editor/Drawers/CopyEntryPropertyDrawer.cs +95 -0
- package/Editor/Drawers/CopyEntryPropertyDrawer.cs.meta +2 -0
- package/Editor/EditorConstants.cs +13 -0
- package/Editor/Inspectors/PackageManifestConfigInspector.cs +31 -0
- package/Editor/PackageManifestConfig.cs +20 -0
- package/Editor/Tools/FileTools.cs +106 -0
- package/Samples~/ClaudeSkills/Editor/ClaudeSkillsPostImport.cs +213 -0
- package/Samples~/ClaudeSkills/Editor/Playdarium.PackageTool.Samples.ClaudeSkills.Editor.asmdef +16 -0
- package/Samples~/ClaudeSkills/unity-package-docs/SKILL.md +309 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/README.md.template +42 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api-chunk.md.template +41 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api-index.md.template +26 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api.md.template +43 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/manual.md.template +57 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/samples.md.template +56 -0
- package/Samples~/ClaudeSkills/unity-package-docs/scripts/scan_package.py +504 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/SKILL.md +309 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/README.md.template +42 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-chunk.md.template +41 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-index.md.template +26 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api.md.template +43 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/manual.md.template +57 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/samples.md.template +56 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/scripts/scan_package.py +504 -0
- package/package.json +9 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- generated by unity-package-docs; safe to regenerate -->
|
|
2
|
+
|
|
3
|
+
# Package Tool — Samples
|
|
4
|
+
|
|
5
|
+
How the sample content in this repository uses the package. Each entry maps a sample file to the package types it exercises, so a reader can recreate the same setup against the public API.
|
|
6
|
+
|
|
7
|
+
## Sample Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Assets/Example/Sample
|
|
11
|
+
├── Prefabs
|
|
12
|
+
│ └── ExamplePrefab.prefab
|
|
13
|
+
└── Scene
|
|
14
|
+
└── SampleScene.unity
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On export, `CopySamplesToDirectory` copies this tree to `Release/Samples~/ExampleSample/`, skipping `.meta` files. The shipped sample is consumable from the Package Manager **Samples** tab on the published package.
|
|
18
|
+
|
|
19
|
+
## Sample → Package Type Mapping
|
|
20
|
+
|
|
21
|
+
### `Assets/Example/Sample/Prefabs/ExamplePrefab.prefab`
|
|
22
|
+
|
|
23
|
+
Kind: prefab
|
|
24
|
+
|
|
25
|
+
This sample asset references no scripts from the package. It exists as a structural demo (the importing user sees a working Scene/Prefab in the package's Samples tab) rather than as an API exercise.
|
|
26
|
+
|
|
27
|
+
### `Assets/Example/Sample/Scene/SampleScene.unity`
|
|
28
|
+
|
|
29
|
+
Kind: scene
|
|
30
|
+
|
|
31
|
+
This sample asset references no scripts from the package. It exists as a structural demo (the importing user sees a working Scene/Prefab in the package's Samples tab) rather than as an API exercise.
|
|
32
|
+
|
|
33
|
+
## Reproducing the Sample
|
|
34
|
+
|
|
35
|
+
The example sample is structural only — there are no MonoBehaviour scripts to instantiate. To reproduce from scratch:
|
|
36
|
+
|
|
37
|
+
1. Create an empty Unity scene and a primitive prefab; both can live anywhere in `Assets/`.
|
|
38
|
+
2. Create a `PackageManifestConfig` asset via **Assets > Create > JCMG/PackageTools/PackageManifestConfig** and configure `sourcePath`, `packageDestinationPath`, and a [`Sample`](api.md#packagemanifestconfigsample) entry whose `sourcePath` points at the folder holding the scene and prefab.
|
|
39
|
+
3. Click **Export Package Source** on the inspector — `FileTools.CreateOrUpdatePackageSource` runs the export pipeline (see `manual.md` → **Export pipeline**) and the sample folder lands under `packageDestinationPath/Samples~/{folderName}`.
|
|
40
|
+
|
|
41
|
+
Minimal equivalent in C#:
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
using PackageTool;
|
|
45
|
+
using PackageTool.Tools;
|
|
46
|
+
using UnityEditor;
|
|
47
|
+
using UnityEngine;
|
|
48
|
+
|
|
49
|
+
var config = ScriptableObject.CreateInstance<PackageManifestConfig>();
|
|
50
|
+
config.packageName = "com.example.sample";
|
|
51
|
+
config.displayName = "Example Sample Package";
|
|
52
|
+
config.packageVersion = "1.0.0";
|
|
53
|
+
config.unityVersion = "2021.3";
|
|
54
|
+
config.sourcePath = "Assets/Example/Source";
|
|
55
|
+
config.packageDestinationPath = "Release";
|
|
56
|
+
config.samples = new[]
|
|
57
|
+
{
|
|
58
|
+
new PackageManifestConfig.Sample
|
|
59
|
+
{
|
|
60
|
+
sourcePath = "Assets/Example/Sample",
|
|
61
|
+
displayName = "Example Sample",
|
|
62
|
+
folderName = "ExampleSample",
|
|
63
|
+
description = "This is example for check test samples",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
AssetDatabase.CreateAsset(config, "Assets/Example/PackageManifestConfig.asset");
|
|
67
|
+
FileTools.CreateOrUpdatePackageSource(config);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- The `Samples~/` folder name (trailing tilde) is a Unity convention: tilde-suffixed folders are excluded from the AssetDatabase import, so packaged samples don't pollute the consuming project until the user clicks **Import** in the Package Manager.
|
|
73
|
+
- For samples that stage content from outside `Assets/` — e.g. a Claude skill folder, vendored data, or generated output — use a [`CopyEntry`](api.md#packagemanifestconfigcopyentry) with `destinationPath` pointing at the same `sourcePath` your `Sample` declares. The `CopyEntry` step runs first; `CopySamplesToDirectory` then ships the staged content into the package.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
using PackageTool.Tools;
|
|
2
|
+
using UnityEditor;
|
|
3
|
+
using UnityEngine;
|
|
4
|
+
|
|
5
|
+
namespace PackageTool.Drawers
|
|
6
|
+
{
|
|
7
|
+
[CustomPropertyDrawer(typeof(PackageManifestConfig.CopyEntry))]
|
|
8
|
+
internal sealed class CopyEntryPropertyDrawer : PropertyDrawer
|
|
9
|
+
{
|
|
10
|
+
private const string SOURCE_PATH_PROPERTY_NAME = "sourcePath";
|
|
11
|
+
private const string DESTINATION_PATH_PROPERTY_NAME = "destinationPath";
|
|
12
|
+
|
|
13
|
+
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
|
14
|
+
{
|
|
15
|
+
var sourceRect = new Rect(position)
|
|
16
|
+
{
|
|
17
|
+
height = EditorConstants.FOLDER_PATH_PICKER_HEIGHT
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
var destinationRect = new Rect(sourceRect)
|
|
21
|
+
{
|
|
22
|
+
position = new Vector2(position.x, sourceRect.y + sourceRect.height + 2f)
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
DrawSourceRow(sourceRect, property.FindPropertyRelative(SOURCE_PATH_PROPERTY_NAME));
|
|
26
|
+
DrawDestinationRow(destinationRect, property.FindPropertyRelative(DESTINATION_PATH_PROPERTY_NAME));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
|
30
|
+
=> EditorConstants.FOLDER_PATH_PICKER_HEIGHT * 2f + 2f;
|
|
31
|
+
|
|
32
|
+
private static void DrawSourceRow(Rect rowRect, SerializedProperty pathProperty)
|
|
33
|
+
{
|
|
34
|
+
var fieldRect = new Rect(rowRect)
|
|
35
|
+
{
|
|
36
|
+
width = rowRect.width - EditorConstants.FOLDER_PATH_PICKER_HEIGHT * 2f
|
|
37
|
+
- EditorConstants.FOLDER_PATH_PICKER_BUFFER
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
EditorGUI.PropertyField(fieldRect, pathProperty, new GUIContent(EditorConstants.COPY_ENTRY_SOURCE_LABEL));
|
|
41
|
+
|
|
42
|
+
var filePickerRect = new Rect
|
|
43
|
+
{
|
|
44
|
+
position = new Vector2(
|
|
45
|
+
fieldRect.x + fieldRect.width + EditorConstants.FOLDER_PATH_PICKER_BUFFER,
|
|
46
|
+
rowRect.y),
|
|
47
|
+
width = EditorConstants.FOLDER_PATH_PICKER_HEIGHT,
|
|
48
|
+
height = EditorConstants.FOLDER_PATH_PICKER_HEIGHT
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
var folderPickerRect = new Rect
|
|
52
|
+
{
|
|
53
|
+
position = new Vector2(
|
|
54
|
+
filePickerRect.x + filePickerRect.width,
|
|
55
|
+
rowRect.y),
|
|
56
|
+
width = EditorConstants.FOLDER_PATH_PICKER_HEIGHT,
|
|
57
|
+
height = EditorConstants.FOLDER_PATH_PICKER_HEIGHT
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
GUILayoutTools.DrawFilePicker(
|
|
61
|
+
filePickerRect,
|
|
62
|
+
pathProperty,
|
|
63
|
+
EditorConstants.SELECT_SOURCE_PATH_FILE_PICKER_TITLE);
|
|
64
|
+
GUILayoutTools.DrawFolderPicker(
|
|
65
|
+
folderPickerRect,
|
|
66
|
+
pathProperty,
|
|
67
|
+
EditorConstants.SELECT_SOURCE_PATH_PICKER_FOLDER_TITLE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static void DrawDestinationRow(Rect rowRect, SerializedProperty pathProperty)
|
|
71
|
+
{
|
|
72
|
+
var fieldRect = new Rect(rowRect)
|
|
73
|
+
{
|
|
74
|
+
width = rowRect.width - EditorConstants.FOLDER_PATH_PICKER_HEIGHT
|
|
75
|
+
- EditorConstants.FOLDER_PATH_PICKER_BUFFER
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
EditorGUI.PropertyField(fieldRect, pathProperty, new GUIContent(EditorConstants.COPY_ENTRY_DESTINATION_LABEL));
|
|
79
|
+
|
|
80
|
+
var folderPickerRect = new Rect
|
|
81
|
+
{
|
|
82
|
+
position = new Vector2(
|
|
83
|
+
fieldRect.x + fieldRect.width + EditorConstants.FOLDER_PATH_PICKER_BUFFER,
|
|
84
|
+
rowRect.y),
|
|
85
|
+
width = EditorConstants.FOLDER_PATH_PICKER_HEIGHT,
|
|
86
|
+
height = EditorConstants.FOLDER_PATH_PICKER_HEIGHT
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
GUILayoutTools.DrawFolderPicker(
|
|
90
|
+
folderPickerRect,
|
|
91
|
+
pathProperty,
|
|
92
|
+
EditorConstants.SELECT_COPY_DESTINATION_FOLDER_PICKER_TITLE);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -105,8 +105,21 @@ namespace PackageTool
|
|
|
105
105
|
public const string PACKAGE_UPDATE_SUCCESS_FORMAT =
|
|
106
106
|
"[Package Tools] Successfully updated package source for [{0}].";
|
|
107
107
|
|
|
108
|
+
public const string AGENT_LOCKED_FILE_MARKER_FORMAT =
|
|
109
|
+
"<!--\n" +
|
|
110
|
+
"AGENT NOTE: This file is auto-generated and locked for direct edits.\n" +
|
|
111
|
+
"Canonical source: ./{0} (at the project root). Edit that file instead.\n" +
|
|
112
|
+
"Regenerated from the canonical source on every package export.\n" +
|
|
113
|
+
"-->\n\n";
|
|
114
|
+
|
|
108
115
|
public const string SAMPLES_FOLDER_NAME = "Samples~";
|
|
109
116
|
public const string SAMPLES_HEADER_LABEL = "Samples";
|
|
110
117
|
public const string SAMPLES_ELEMENT_LABEL_FORMAT = "Sample {0}:";
|
|
118
|
+
|
|
119
|
+
public const string COPY_ENTRIES_HEADER_LABEL = "Copy Entries";
|
|
120
|
+
public const string COPY_ENTRY_ELEMENT_LABEL_FORMAT = "Copy {0}:";
|
|
121
|
+
public const string COPY_ENTRY_SOURCE_LABEL = "Source";
|
|
122
|
+
public const string COPY_ENTRY_DESTINATION_LABEL = "Destination";
|
|
123
|
+
public const string SELECT_COPY_DESTINATION_FOLDER_PICKER_TITLE = "Select Copy Destination Folder";
|
|
111
124
|
}
|
|
112
125
|
}
|
|
@@ -37,6 +37,7 @@ namespace PackageTool.Inspectors
|
|
|
37
37
|
private ReorderableList _keywordReorderableList;
|
|
38
38
|
private ReorderableList _dependenciesReorderableList;
|
|
39
39
|
private ReorderableList _samplesReorderableList;
|
|
40
|
+
private ReorderableList _copyEntriesReorderableList;
|
|
40
41
|
|
|
41
42
|
private const string SOURCE_PATH_PROPERTY_NAME = "sourcePath";
|
|
42
43
|
private const string DOCUMENTATION_PATH_PROPERTY_NAME = "documentationPath";
|
|
@@ -61,6 +62,7 @@ namespace PackageTool.Inspectors
|
|
|
61
62
|
private const string VERSION_CONSTANTS_NAMESPACE_PROPERTY_NAME = "versionConstantsNamespace";
|
|
62
63
|
private const string ID_PROPERTY_NAME = "_id";
|
|
63
64
|
private const string SAMPLES_PROPERTY_NAME = "samples";
|
|
65
|
+
private const string COPY_ENTRIES_PROPERTY_NAME = "copyEntries";
|
|
64
66
|
|
|
65
67
|
private void OnEnable()
|
|
66
68
|
{
|
|
@@ -109,6 +111,16 @@ namespace PackageTool.Inspectors
|
|
|
109
111
|
drawElementCallback = DrawSampleElement,
|
|
110
112
|
elementHeight = EditorGUIUtility.singleLineHeight * 5f
|
|
111
113
|
};
|
|
114
|
+
|
|
115
|
+
var copyEntriesProp = serializedObject.FindProperty(COPY_ENTRIES_PROPERTY_NAME);
|
|
116
|
+
_copyEntriesReorderableList = new ReorderableList(
|
|
117
|
+
serializedObject,
|
|
118
|
+
copyEntriesProp)
|
|
119
|
+
{
|
|
120
|
+
drawHeaderCallback = DrawCopyEntriesHeader,
|
|
121
|
+
drawElementCallback = DrawCopyEntryElement,
|
|
122
|
+
elementHeight = EditorConstants.FOLDER_PATH_PICKER_HEIGHT * 2f + 2f
|
|
123
|
+
};
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
public override void OnInspectorGUI()
|
|
@@ -203,6 +215,7 @@ namespace PackageTool.Inspectors
|
|
|
203
215
|
EditorGUILayout.Space();
|
|
204
216
|
// _sourcePathsReorderableList.DoLayoutList();
|
|
205
217
|
_excludePathsReorderableList.DoLayoutList();
|
|
218
|
+
_copyEntriesReorderableList.DoLayoutList();
|
|
206
219
|
|
|
207
220
|
// Package Source Export
|
|
208
221
|
using (new EditorGUILayout.HorizontalScope())
|
|
@@ -400,5 +413,23 @@ namespace PackageTool.Inspectors
|
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
#endregion
|
|
416
|
+
|
|
417
|
+
#region Copy Entries ReorderableList
|
|
418
|
+
|
|
419
|
+
private void DrawCopyEntriesHeader(Rect rect)
|
|
420
|
+
{
|
|
421
|
+
EditorGUI.LabelField(rect, EditorConstants.COPY_ENTRIES_HEADER_LABEL, EditorStyles.boldLabel);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private void DrawCopyEntryElement(Rect rect, int index, bool isActive, bool isFocused)
|
|
425
|
+
{
|
|
426
|
+
EditorGUI.PropertyField(
|
|
427
|
+
rect,
|
|
428
|
+
serializedObject.FindProperty(COPY_ENTRIES_PROPERTY_NAME).GetArrayElementAtIndex(index),
|
|
429
|
+
new GUIContent(string.Format(EditorConstants.COPY_ENTRY_ELEMENT_LABEL_FORMAT, index))
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#endregion
|
|
403
434
|
}
|
|
404
435
|
}
|
|
@@ -82,6 +82,21 @@ namespace PackageTool
|
|
|
82
82
|
|| string.IsNullOrEmpty(folderName);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/// <summary>
|
|
86
|
+
/// Describes a copy operation that mirrors a source file or folder into a destination folder inside the
|
|
87
|
+
/// project. When the source is a folder, the same-named subfolder of the destination is replaced with the
|
|
88
|
+
/// source folder's content. When the source is a file, the same-named file inside the destination folder
|
|
89
|
+
/// is replaced. An empty or non-existent destination path falls back to the project root.
|
|
90
|
+
/// </summary>
|
|
91
|
+
[Serializable]
|
|
92
|
+
public sealed class CopyEntry
|
|
93
|
+
{
|
|
94
|
+
public string sourcePath;
|
|
95
|
+
public string destinationPath;
|
|
96
|
+
|
|
97
|
+
public bool IsEmpty() => string.IsNullOrEmpty(sourcePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
/// <summary>
|
|
86
101
|
/// A unique id for this <see cref="PackageManifestConfig"/> instance.
|
|
87
102
|
/// </summary>
|
|
@@ -162,6 +177,11 @@ namespace PackageTool
|
|
|
162
177
|
|
|
163
178
|
public Sample[] samples;
|
|
164
179
|
|
|
180
|
+
/// <summary>
|
|
181
|
+
/// A collection of additional source-to-destination copy operations performed during export.
|
|
182
|
+
/// </summary>
|
|
183
|
+
public CopyEntry[] copyEntries;
|
|
184
|
+
|
|
165
185
|
/// <summary>
|
|
166
186
|
/// A path to the where the VersionConstants.cs file should be created/updated
|
|
167
187
|
/// </summary>
|
|
@@ -122,9 +122,11 @@ namespace PackageTool.Tools
|
|
|
122
122
|
// If its a file, copy over it and its meta file if it exists.
|
|
123
123
|
var normalizedSourcePath = Path.GetFullPath(packageManifest.sourcePath);
|
|
124
124
|
|
|
125
|
+
CopyEntriesToProject(packageManifest);
|
|
125
126
|
CopyDocumentationToDirectory(packageManifest);
|
|
126
127
|
RecursivelyCopyDirectoriesAndFiles(packageManifest, normalizedSourcePath, normalizedDestinationPath);
|
|
127
128
|
CopySamplesToDirectory(packageManifest);
|
|
129
|
+
StampLocalDocCopiesForAgents(packageManifest);
|
|
128
130
|
|
|
129
131
|
Debug.LogFormat(EditorConstants.PACKAGE_UPDATE_SUCCESS_FORMAT, packageManifest.packageName);
|
|
130
132
|
|
|
@@ -192,6 +194,110 @@ namespace PackageTool.Tools
|
|
|
192
194
|
File.Copy(normalizedSourcePath, destinationPath);
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/// <summary>
|
|
198
|
+
/// Stages each configured <see cref="PackageManifestConfig.CopyEntry"/> into the project before the
|
|
199
|
+
/// rest of the export runs. A folder source's content is merged directly into
|
|
200
|
+
/// <see cref="PackageManifestConfig.CopyEntry.destinationPath"/> (same-named files are overwritten,
|
|
201
|
+
/// unrelated files in the destination are left alone). A file source overwrites
|
|
202
|
+
/// <c>destination/{filename}</c>. A blank destination resolves to the project root; a non-blank
|
|
203
|
+
/// destination that does not yet exist is created. Missing sources log a warning and are skipped.
|
|
204
|
+
/// </summary>
|
|
205
|
+
public static void CopyEntriesToProject(PackageManifestConfig packageManifest)
|
|
206
|
+
{
|
|
207
|
+
var entries = packageManifest.copyEntries;
|
|
208
|
+
if (entries == null || entries.Length == 0)
|
|
209
|
+
return;
|
|
210
|
+
|
|
211
|
+
foreach (var entry in entries)
|
|
212
|
+
{
|
|
213
|
+
if (entry.IsEmpty())
|
|
214
|
+
continue;
|
|
215
|
+
|
|
216
|
+
var normalizedSourcePath = Path.GetFullPath(entry.sourcePath);
|
|
217
|
+
var destinationDirectory = ResolveDestinationDirectory(entry.destinationPath);
|
|
218
|
+
|
|
219
|
+
if (!Directory.Exists(destinationDirectory))
|
|
220
|
+
Directory.CreateDirectory(destinationDirectory);
|
|
221
|
+
|
|
222
|
+
if (!Directory.Exists(normalizedSourcePath) && !File.Exists(normalizedSourcePath))
|
|
223
|
+
{
|
|
224
|
+
Debug.LogWarningFormat(
|
|
225
|
+
"[Package Tools] Copy entry source [{0}] does not exist, skipping.",
|
|
226
|
+
entry.sourcePath);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (Directory.Exists(normalizedSourcePath))
|
|
231
|
+
{
|
|
232
|
+
CopyDirectoryRecursively(normalizedSourcePath, destinationDirectory);
|
|
233
|
+
}
|
|
234
|
+
else if (File.Exists(normalizedSourcePath))
|
|
235
|
+
{
|
|
236
|
+
var sourceName = Path.GetFileName(normalizedSourcePath);
|
|
237
|
+
var targetPath = Path.Combine(destinationDirectory, sourceName);
|
|
238
|
+
|
|
239
|
+
if (File.Exists(targetPath))
|
|
240
|
+
File.Delete(targetPath);
|
|
241
|
+
|
|
242
|
+
File.Copy(normalizedSourcePath, targetPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private static string ResolveDestinationDirectory(string rawDestinationPath)
|
|
248
|
+
{
|
|
249
|
+
if (string.IsNullOrEmpty(rawDestinationPath))
|
|
250
|
+
return EditorConstants.ProjectPath;
|
|
251
|
+
|
|
252
|
+
return Path.GetFullPath(rawDestinationPath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private static void CopyDirectoryRecursively(string normalizedSourcePath, string normalizedDestinationPath)
|
|
256
|
+
{
|
|
257
|
+
var directoryInfo = new DirectoryInfo(normalizedSourcePath);
|
|
258
|
+
|
|
259
|
+
foreach (var sdi in directoryInfo.GetDirectories(EditorConstants.WILDCARD_FILTER, SearchOption.AllDirectories))
|
|
260
|
+
Directory.CreateDirectory(sdi.FullName.Replace(normalizedSourcePath, normalizedDestinationPath));
|
|
261
|
+
|
|
262
|
+
foreach (var fi in directoryInfo.GetFiles(EditorConstants.WILDCARD_FILTER, SearchOption.AllDirectories))
|
|
263
|
+
{
|
|
264
|
+
var newPath = fi.FullName.Replace(normalizedSourcePath, normalizedDestinationPath);
|
|
265
|
+
File.Copy(fi.FullName, newPath, overwrite: true);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// <summary>
|
|
270
|
+
/// Prepends an HTML-comment AGENT NOTE to the markdown documentation copies under
|
|
271
|
+
/// <c>sourcePath</c> so any tool or agent that opens the local artifact sees it is locked
|
|
272
|
+
/// and is pointed at the canonical project-root file. Runs after the recursive copy to
|
|
273
|
+
/// <c>packageDestinationPath</c>, so the shipped package in <c>Release/</c> stays unstamped
|
|
274
|
+
/// and consumers never see the note. Plain-text companions like <c>LICENSE</c> are skipped —
|
|
275
|
+
/// a comment prefix would visibly modify the legal notice.
|
|
276
|
+
/// </summary>
|
|
277
|
+
private static void StampLocalDocCopiesForAgents(PackageManifestConfig packageManifest)
|
|
278
|
+
{
|
|
279
|
+
var sourceDirectory = Path.GetFullPath(packageManifest.sourcePath);
|
|
280
|
+
StampMarkdownCopy(sourceDirectory, packageManifest.readmePath);
|
|
281
|
+
StampMarkdownCopy(sourceDirectory, packageManifest.changelogPath);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private static void StampMarkdownCopy(string sourceDirectory, string canonicalRelativePath)
|
|
285
|
+
{
|
|
286
|
+
if (string.IsNullOrEmpty(canonicalRelativePath))
|
|
287
|
+
return;
|
|
288
|
+
if (!canonicalRelativePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
|
|
289
|
+
return;
|
|
290
|
+
|
|
291
|
+
var fileName = Path.GetFileName(canonicalRelativePath);
|
|
292
|
+
var localCopyPath = Path.Combine(sourceDirectory, fileName);
|
|
293
|
+
if (!File.Exists(localCopyPath))
|
|
294
|
+
return;
|
|
295
|
+
|
|
296
|
+
var marker = string.Format(EditorConstants.AGENT_LOCKED_FILE_MARKER_FORMAT, canonicalRelativePath);
|
|
297
|
+
var existing = File.ReadAllText(localCopyPath);
|
|
298
|
+
File.WriteAllText(localCopyPath, marker + existing);
|
|
299
|
+
}
|
|
300
|
+
|
|
195
301
|
private static void CopySamplesToDirectory(PackageManifestConfig packageManifest)
|
|
196
302
|
{
|
|
197
303
|
var samples = packageManifest.samples;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using UnityEditor;
|
|
5
|
+
using UnityEngine;
|
|
6
|
+
|
|
7
|
+
namespace PackageTool.Samples.ClaudeSkills
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Runs once after the "Claude Skills" sample is imported into a consumer project. Walks every
|
|
11
|
+
/// skill subfolder next to this script, copies it to <c><project>/.claude/skills/<name></c>,
|
|
12
|
+
/// prompting before overwriting an existing skill, and then deletes the imported sample folder so
|
|
13
|
+
/// the operation does not repeat on subsequent domain reloads.
|
|
14
|
+
/// </summary>
|
|
15
|
+
[InitializeOnLoad]
|
|
16
|
+
internal static class ClaudeSkillsPostImport
|
|
17
|
+
{
|
|
18
|
+
private const string LOG_PREFIX = "[Claude Skills]";
|
|
19
|
+
private const string SKILLS_DESTINATION_RELATIVE = ".claude/skills";
|
|
20
|
+
private const string EDITOR_FOLDER_NAME = "Editor";
|
|
21
|
+
private const string SCRIPT_FILE_NAME = "ClaudeSkillsPostImport.cs";
|
|
22
|
+
private const string DIALOG_TITLE = "Claude Skills";
|
|
23
|
+
private const string SESSION_GUARD_KEY_PREFIX = "PackageTool.ClaudeSkillsPostImport.Processed:";
|
|
24
|
+
private const string SESSION_RUNNING_KEY = "PackageTool.ClaudeSkillsPostImport.Running";
|
|
25
|
+
|
|
26
|
+
static ClaudeSkillsPostImport()
|
|
27
|
+
{
|
|
28
|
+
EditorApplication.delayCall += Run;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static void Run()
|
|
32
|
+
{
|
|
33
|
+
// Re-entrancy guard. Multiple copies of this type (e.g. in side-by-side sample imports
|
|
34
|
+
// each shipping their own assembly) would each schedule a delayCall; without this guard
|
|
35
|
+
// they would race on the same sample folders and double-prompt the user.
|
|
36
|
+
if (SessionState.GetBool(SESSION_RUNNING_KEY, false))
|
|
37
|
+
return;
|
|
38
|
+
SessionState.SetBool(SESSION_RUNNING_KEY, true);
|
|
39
|
+
|
|
40
|
+
try
|
|
41
|
+
{
|
|
42
|
+
foreach (var sampleRootAssetPath in FindSampleRootAssetPaths())
|
|
43
|
+
{
|
|
44
|
+
// Only run from an imported sample location; never act on the source folder
|
|
45
|
+
// inside a package author's project.
|
|
46
|
+
if (!sampleRootAssetPath.StartsWith("Assets/Samples/", StringComparison.Ordinal))
|
|
47
|
+
continue;
|
|
48
|
+
|
|
49
|
+
var guardKey = SESSION_GUARD_KEY_PREFIX + sampleRootAssetPath;
|
|
50
|
+
if (SessionState.GetBool(guardKey, false))
|
|
51
|
+
continue;
|
|
52
|
+
SessionState.SetBool(guardKey, true);
|
|
53
|
+
|
|
54
|
+
ProcessSample(sampleRootAssetPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (Exception e)
|
|
58
|
+
{
|
|
59
|
+
Debug.LogErrorFormat("{0} Post-import failed: {1}", LOG_PREFIX, e);
|
|
60
|
+
}
|
|
61
|
+
finally
|
|
62
|
+
{
|
|
63
|
+
SessionState.SetBool(SESSION_RUNNING_KEY, false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static IEnumerable<string> FindSampleRootAssetPaths()
|
|
68
|
+
{
|
|
69
|
+
var suffix = "/" + EDITOR_FOLDER_NAME + "/" + SCRIPT_FILE_NAME;
|
|
70
|
+
var guids = AssetDatabase.FindAssets("ClaudeSkillsPostImport t:MonoScript");
|
|
71
|
+
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
72
|
+
foreach (var guid in guids)
|
|
73
|
+
{
|
|
74
|
+
var path = AssetDatabase.GUIDToAssetPath(guid);
|
|
75
|
+
if (string.IsNullOrEmpty(path) || !path.EndsWith(suffix, StringComparison.Ordinal))
|
|
76
|
+
continue;
|
|
77
|
+
|
|
78
|
+
var sampleRoot = path.Substring(0, path.Length - suffix.Length);
|
|
79
|
+
if (seen.Add(sampleRoot))
|
|
80
|
+
yield return sampleRoot;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static void ProcessSample(string sampleRootAssetPath)
|
|
85
|
+
{
|
|
86
|
+
var projectRoot = ResolveProjectRoot();
|
|
87
|
+
var skillsRoot = Path.Combine(projectRoot, SKILLS_DESTINATION_RELATIVE);
|
|
88
|
+
var fullSampleRoot = Path.GetFullPath(sampleRootAssetPath);
|
|
89
|
+
|
|
90
|
+
if (!Directory.Exists(fullSampleRoot))
|
|
91
|
+
{
|
|
92
|
+
Debug.LogWarningFormat("{0} Sample folder not found at [{1}].", LOG_PREFIX, fullSampleRoot);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Directory.Exists(skillsRoot))
|
|
97
|
+
Directory.CreateDirectory(skillsRoot);
|
|
98
|
+
|
|
99
|
+
Debug.LogFormat(
|
|
100
|
+
"{0} Installing skills from [{1}] into [{2}].",
|
|
101
|
+
LOG_PREFIX,
|
|
102
|
+
sampleRootAssetPath,
|
|
103
|
+
ToProjectRelative(skillsRoot, projectRoot));
|
|
104
|
+
|
|
105
|
+
var installed = 0;
|
|
106
|
+
var replaced = 0;
|
|
107
|
+
var skipped = 0;
|
|
108
|
+
|
|
109
|
+
foreach (var skillSource in Directory.GetDirectories(fullSampleRoot))
|
|
110
|
+
{
|
|
111
|
+
var skillName = Path.GetFileName(skillSource);
|
|
112
|
+
if (string.Equals(skillName, EDITOR_FOLDER_NAME, StringComparison.Ordinal))
|
|
113
|
+
continue;
|
|
114
|
+
|
|
115
|
+
var skillTarget = Path.Combine(skillsRoot, skillName);
|
|
116
|
+
if (Directory.Exists(skillTarget))
|
|
117
|
+
{
|
|
118
|
+
var replace = EditorUtility.DisplayDialog(
|
|
119
|
+
DIALOG_TITLE,
|
|
120
|
+
string.Format(
|
|
121
|
+
"Skill \"{0}\" already exists at {1}.\n\nReplace it with the imported version?",
|
|
122
|
+
skillName,
|
|
123
|
+
ToProjectRelative(skillTarget, projectRoot)),
|
|
124
|
+
"Replace",
|
|
125
|
+
"Skip");
|
|
126
|
+
|
|
127
|
+
if (!replace)
|
|
128
|
+
{
|
|
129
|
+
Debug.LogFormat("{0} Skipped \"{1}\" (already exists).", LOG_PREFIX, skillName);
|
|
130
|
+
skipped++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Directory.Delete(skillTarget, recursive: true);
|
|
135
|
+
replaced++;
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
{
|
|
139
|
+
installed++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
CopySkillDirectory(skillSource, skillTarget);
|
|
143
|
+
|
|
144
|
+
Debug.LogFormat(
|
|
145
|
+
"{0} Installed \"{1}\" -> {2}.",
|
|
146
|
+
LOG_PREFIX,
|
|
147
|
+
skillName,
|
|
148
|
+
ToProjectRelative(skillTarget, projectRoot));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Debug.LogFormat(
|
|
152
|
+
"{0} Done. Installed: {1}, Replaced: {2}, Skipped: {3}.",
|
|
153
|
+
LOG_PREFIX, installed, replaced, skipped);
|
|
154
|
+
|
|
155
|
+
AssetDatabase.DeleteAsset(sampleRootAssetPath);
|
|
156
|
+
AssetDatabase.Refresh();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private static void CopySkillDirectory(string source, string destination)
|
|
160
|
+
{
|
|
161
|
+
Directory.CreateDirectory(destination);
|
|
162
|
+
|
|
163
|
+
foreach (var dir in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
|
|
164
|
+
{
|
|
165
|
+
var relative = MakeRelative(source, dir);
|
|
166
|
+
Directory.CreateDirectory(Path.Combine(destination, relative));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
|
|
170
|
+
{
|
|
171
|
+
// Skip Unity meta files; the destination lives outside Assets/ and doesn't need them.
|
|
172
|
+
if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase))
|
|
173
|
+
continue;
|
|
174
|
+
|
|
175
|
+
var relative = MakeRelative(source, file);
|
|
176
|
+
var targetPath = Path.Combine(destination, relative);
|
|
177
|
+
var targetDir = Path.GetDirectoryName(targetPath);
|
|
178
|
+
if (!string.IsNullOrEmpty(targetDir))
|
|
179
|
+
Directory.CreateDirectory(targetDir);
|
|
180
|
+
|
|
181
|
+
File.Copy(file, targetPath, overwrite: true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static string MakeRelative(string root, string fullPath)
|
|
186
|
+
{
|
|
187
|
+
var rel = fullPath.Substring(root.Length);
|
|
188
|
+
return rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private static string ResolveProjectRoot()
|
|
192
|
+
{
|
|
193
|
+
const string assetsSuffix = "/Assets";
|
|
194
|
+
var dataPath = Application.dataPath;
|
|
195
|
+
if (dataPath.EndsWith(assetsSuffix, StringComparison.Ordinal))
|
|
196
|
+
return dataPath.Substring(0, dataPath.Length - assetsSuffix.Length);
|
|
197
|
+
|
|
198
|
+
var parent = Directory.GetParent(dataPath);
|
|
199
|
+
return parent != null ? parent.FullName : dataPath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private static string ToProjectRelative(string fullPath, string projectRoot)
|
|
203
|
+
{
|
|
204
|
+
var normalized = fullPath.Replace('\\', '/');
|
|
205
|
+
var rootNormalized = projectRoot.Replace('\\', '/');
|
|
206
|
+
if (!normalized.StartsWith(rootNormalized, StringComparison.Ordinal))
|
|
207
|
+
return normalized;
|
|
208
|
+
|
|
209
|
+
var rel = normalized.Substring(rootNormalized.Length).TrimStart('/');
|
|
210
|
+
return string.IsNullOrEmpty(rel) ? "." : rel;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/Samples~/ClaudeSkills/Editor/Playdarium.PackageTool.Samples.ClaudeSkills.Editor.asmdef
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Playdarium.PackageTool.Samples.ClaudeSkills.Editor",
|
|
3
|
+
"rootNamespace": "",
|
|
4
|
+
"references": [],
|
|
5
|
+
"includePlatforms": [
|
|
6
|
+
"Editor"
|
|
7
|
+
],
|
|
8
|
+
"excludePlatforms": [],
|
|
9
|
+
"allowUnsafeCode": false,
|
|
10
|
+
"overrideReferences": false,
|
|
11
|
+
"precompiledReferences": [],
|
|
12
|
+
"autoReferenced": true,
|
|
13
|
+
"defineConstraints": [],
|
|
14
|
+
"versionDefines": [],
|
|
15
|
+
"noEngineReferences": false
|
|
16
|
+
}
|