com.elestrago.unity.package-tools 2.1.0 → 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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## [2.2.0](https://gitlab.com/elestrago-pkg/package-tool/-/tags/2.2.0)
6
+
7
+ ### Added
8
+
9
+ - Added post-import hook for the `Claude Skills` sample that moves imported skills into `./.claude/skills` and prompts before overwriting existing skills.
10
+ - 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.
11
+
12
+ ---
13
+
5
14
  ## [2.1.0](https://gitlab.com/elestrago-pkg/package-tool/-/tags/2.1.0)
6
15
 
7
16
  ### 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,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>&lt;project&gt;/.claude/skills/&lt;name&gt;</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
+ }
@@ -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.0",
3
+ "version": "2.2.0",
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.0/README.md",
10
- "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1.0/CHANGELOG.md",
11
- "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1.0/LICENSE",
9
+ "documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.0/README.md",
10
+ "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.0/CHANGELOG.md",
11
+ "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.0/LICENSE",
12
12
  "license": "MIT",
13
13
  "keywords": [
14
14
  "unity",