com.elestrago.unity.package-tools 2.1.0 → 2.2.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.
- package/CAHNGELOG.md +17 -0
- package/Editor/EditorConstants.cs +7 -0
- package/Editor/Tools/FileTools.cs +33 -0
- package/Samples~/ClaudeSkills/Editor/ClaudeSkillsPostImport.cs +259 -0
- package/Samples~/ClaudeSkills/Editor/Playdarium.PackageTool.Samples.ClaudeSkills.Editor.asmdef +16 -0
- package/package.json +4 -4
package/CAHNGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [2.2.1](https://gitlab.com/elestrago-pkg/package-tool/-/tags/2.2.1)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added cleanup of empty ancestor folders under `Assets/Samples/` after the `Claude Skills` post-import hook finishes, so importing the sample leaves no stub `Assets/Samples/<Package>/<Version>/` directories behind.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## [2.2.0](https://gitlab.com/elestrago-pkg/package-tool/-/tags/2.2.0)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added post-import hook for the `Claude Skills` sample that moves imported skills into `./.claude/skills` and prompts before overwriting existing skills.
|
|
18
|
+
- Added an AGENT NOTE marker stamped onto the local `README.md` and `CAHNGELOG.md` export artifacts after each `Export Package Source`, pointing edits back to the project-root canonical files.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
5
22
|
## [2.1.0](https://gitlab.com/elestrago-pkg/package-tool/-/tags/2.1.0)
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -105,6 +105,13 @@ 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}:";
|
|
@@ -126,6 +126,7 @@ namespace PackageTool.Tools
|
|
|
126
126
|
CopyDocumentationToDirectory(packageManifest);
|
|
127
127
|
RecursivelyCopyDirectoriesAndFiles(packageManifest, normalizedSourcePath, normalizedDestinationPath);
|
|
128
128
|
CopySamplesToDirectory(packageManifest);
|
|
129
|
+
StampLocalDocCopiesForAgents(packageManifest);
|
|
129
130
|
|
|
130
131
|
Debug.LogFormat(EditorConstants.PACKAGE_UPDATE_SUCCESS_FORMAT, packageManifest.packageName);
|
|
131
132
|
|
|
@@ -265,6 +266,38 @@ namespace PackageTool.Tools
|
|
|
265
266
|
}
|
|
266
267
|
}
|
|
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
|
+
|
|
268
301
|
private static void CopySamplesToDirectory(PackageManifestConfig packageManifest)
|
|
269
302
|
{
|
|
270
303
|
var samples = packageManifest.samples;
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
DeleteSampleAndEmptyAncestors(sampleRootAssetPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private static void DeleteSampleAndEmptyAncestors(string sampleRootAssetPath)
|
|
159
|
+
{
|
|
160
|
+
if (!AssetDatabase.DeleteAsset(sampleRootAssetPath))
|
|
161
|
+
{
|
|
162
|
+
Debug.LogWarningFormat(
|
|
163
|
+
"{0} Could not remove imported sample folder [{1}].",
|
|
164
|
+
LOG_PREFIX,
|
|
165
|
+
sampleRootAssetPath);
|
|
166
|
+
AssetDatabase.Refresh();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Walk up Assets/Samples/<Package>/<Version>/<Sample>, removing each ancestor that the
|
|
171
|
+
// just-deleted sample left empty. Bounded to the Assets/Samples subtree so we never
|
|
172
|
+
// touch unrelated project folders.
|
|
173
|
+
var current = GetParentAssetPath(sampleRootAssetPath);
|
|
174
|
+
while (!string.IsNullOrEmpty(current) && IsWithinSamplesTree(current))
|
|
175
|
+
{
|
|
176
|
+
var fullPath = Path.GetFullPath(current);
|
|
177
|
+
if (!Directory.Exists(fullPath))
|
|
178
|
+
break;
|
|
179
|
+
if (Directory.GetFileSystemEntries(fullPath).Length > 0)
|
|
180
|
+
break;
|
|
181
|
+
if (!AssetDatabase.DeleteAsset(current))
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
Debug.LogFormat("{0} Removed empty folder [{1}].", LOG_PREFIX, current);
|
|
185
|
+
current = GetParentAssetPath(current);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
AssetDatabase.Refresh();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private static string GetParentAssetPath(string assetPath)
|
|
192
|
+
{
|
|
193
|
+
if (string.IsNullOrEmpty(assetPath))
|
|
194
|
+
return null;
|
|
195
|
+
var lastSlash = assetPath.LastIndexOf('/');
|
|
196
|
+
return lastSlash <= 0 ? null : assetPath.Substring(0, lastSlash);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private static bool IsWithinSamplesTree(string assetPath)
|
|
200
|
+
{
|
|
201
|
+
return string.Equals(assetPath, "Assets/Samples", StringComparison.Ordinal)
|
|
202
|
+
|| assetPath.StartsWith("Assets/Samples/", StringComparison.Ordinal);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static void CopySkillDirectory(string source, string destination)
|
|
206
|
+
{
|
|
207
|
+
Directory.CreateDirectory(destination);
|
|
208
|
+
|
|
209
|
+
foreach (var dir in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
|
|
210
|
+
{
|
|
211
|
+
var relative = MakeRelative(source, dir);
|
|
212
|
+
Directory.CreateDirectory(Path.Combine(destination, relative));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
|
|
216
|
+
{
|
|
217
|
+
// Skip Unity meta files; the destination lives outside Assets/ and doesn't need them.
|
|
218
|
+
if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase))
|
|
219
|
+
continue;
|
|
220
|
+
|
|
221
|
+
var relative = MakeRelative(source, file);
|
|
222
|
+
var targetPath = Path.Combine(destination, relative);
|
|
223
|
+
var targetDir = Path.GetDirectoryName(targetPath);
|
|
224
|
+
if (!string.IsNullOrEmpty(targetDir))
|
|
225
|
+
Directory.CreateDirectory(targetDir);
|
|
226
|
+
|
|
227
|
+
File.Copy(file, targetPath, overwrite: true);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private static string MakeRelative(string root, string fullPath)
|
|
232
|
+
{
|
|
233
|
+
var rel = fullPath.Substring(root.Length);
|
|
234
|
+
return rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private static string ResolveProjectRoot()
|
|
238
|
+
{
|
|
239
|
+
const string assetsSuffix = "/Assets";
|
|
240
|
+
var dataPath = Application.dataPath;
|
|
241
|
+
if (dataPath.EndsWith(assetsSuffix, StringComparison.Ordinal))
|
|
242
|
+
return dataPath.Substring(0, dataPath.Length - assetsSuffix.Length);
|
|
243
|
+
|
|
244
|
+
var parent = Directory.GetParent(dataPath);
|
|
245
|
+
return parent != null ? parent.FullName : dataPath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static string ToProjectRelative(string fullPath, string projectRoot)
|
|
249
|
+
{
|
|
250
|
+
var normalized = fullPath.Replace('\\', '/');
|
|
251
|
+
var rootNormalized = projectRoot.Replace('\\', '/');
|
|
252
|
+
if (!normalized.StartsWith(rootNormalized, StringComparison.Ordinal))
|
|
253
|
+
return normalized;
|
|
254
|
+
|
|
255
|
+
var rel = normalized.Substring(rootNormalized.Length).TrimStart('/');
|
|
256
|
+
return string.IsNullOrEmpty(rel) ? "." : rel;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.elestrago.unity.package-tools",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"displayName": "Package Tool",
|
|
5
5
|
"description": "Tool for create unity packages",
|
|
6
6
|
"category": "unity",
|
|
7
7
|
"unity": "2021.3",
|
|
8
8
|
"homepage": "https://gitlab.com/elestrago-pkg/package-tool",
|
|
9
|
-
"documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1
|
|
10
|
-
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1
|
|
11
|
-
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1
|
|
9
|
+
"documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.1/README.md",
|
|
10
|
+
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.1/CHANGELOG.md",
|
|
11
|
+
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.1/LICENSE",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"unity",
|