com.elestrago.unity.package-tools 2.3.0 → 2.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.
- package/CAHNGELOG.md +37 -0
- package/Documentation~/examples/ciutils-generate.md +41 -0
- package/Documentation~/examples/create-packagemanifestconfig.md +47 -0
- package/Documentation~/examples/init-package.md +30 -0
- package/Documentation~/examples/preparedll.md +39 -0
- package/Documentation~/examples.md +25 -0
- package/Documentation~/manual.md +4 -4
- package/Editor/Tools/CommandLineTools.cs +2 -2
- package/Editor/Tools/GitTools.cs +7 -13
- package/Editor/Utils/PackageInitialize/PackageInitializeTemplates.cs +1 -1
- package/Editor/Utils/PackageInitialize/PackageInitializeUtil.cs +1 -1
- package/README.md +2 -2
- package/Samples~/ClaudeSkills/unity-package-docs/SKILL.md +104 -49
- package/Samples~/ClaudeSkills/unity-package-docs/assets/examples-chunk.md.template +40 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/examples.md.template +36 -0
- package/Samples~/ClaudeSkills/unity-package-docs/scripts/scan_package.py +5 -1
- package/Samples~/ClaudeSkills/unity-package-release/SKILL.md +373 -0
- package/Samples~/ClaudeSkills/unity-package-release/scripts/discover_package.py +366 -0
- package/package.json +4 -4
- package/Documentation~/samples.md +0 -107
- package/Samples~/ClaudeSkills/unity-package-docs/assets/samples.md.template +0 -56
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Discover the PackageManifestConfig.asset in a Unity-package repo and report
|
|
2
|
+
the fields and files the release skill needs to drive its pipeline.
|
|
3
|
+
|
|
4
|
+
The skill must not hardcode paths or line numbers because it is shipped to
|
|
5
|
+
multiple repos that all use the same `com.elestrago.unity.package-tools` editor
|
|
6
|
+
tool but differ in their layout, package name, changelog filename, README
|
|
7
|
+
location, and publish flow. Everything repo-specific is read at runtime.
|
|
8
|
+
|
|
9
|
+
Run from the repo root (default) or pass `--repo <path>`:
|
|
10
|
+
|
|
11
|
+
python3 .claude/skills/unity-package-release/scripts/discover_package.py
|
|
12
|
+
|
|
13
|
+
Output is a single JSON object on stdout. Errors go to stderr; non-zero exit
|
|
14
|
+
codes mean the caller cannot proceed safely.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# A PackageManifestConfig asset is a Unity ScriptableObject with a recognisable
|
|
30
|
+
# set of fields. We detect by signature rather than by path, since the canonical
|
|
31
|
+
# location varies between repos (the package-tool repo keeps it under
|
|
32
|
+
# Assets/PackageManifest/, but consumers are free to put it anywhere under
|
|
33
|
+
# Assets/).
|
|
34
|
+
CONFIG_SIGNATURE_FIELDS = (
|
|
35
|
+
"packageName:",
|
|
36
|
+
"packageVersion:",
|
|
37
|
+
"packageDestinationPath:",
|
|
38
|
+
"changelogPath:",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Top-level SO fields in Unity YAML are indented two spaces under `MonoBehaviour:`.
|
|
42
|
+
# Dependency entries are inside a `dependencies:` list and indent four spaces, so
|
|
43
|
+
# the rule "first line matching exactly `^ <field>: `" reliably picks the SO's
|
|
44
|
+
# own value and skips nested `packageVersion:` entries inside dependencies.
|
|
45
|
+
TOP_LEVEL_FIELD_RE = re.compile(r"^ (?P<field>[A-Za-z_][A-Za-z0-9_]*): ?(?P<value>.*)$")
|
|
46
|
+
|
|
47
|
+
# Inline-list `[a, b, c]` and `[]` show up for string-array fields. We don't
|
|
48
|
+
# parse them here — none of the fields the release skill cares about are arrays
|
|
49
|
+
# of primitives — but the regex above tolerates them by capturing the whole
|
|
50
|
+
# right-hand side as a string.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_config_asset(repo: Path) -> Path | None:
|
|
54
|
+
"""Walk `<repo>/Assets/` for the first `.asset` file whose contents contain
|
|
55
|
+
all of the PackageManifestConfig signature fields. Returns None when no
|
|
56
|
+
candidate is found — the caller should refuse to proceed in that case."""
|
|
57
|
+
assets_dir = repo / "Assets"
|
|
58
|
+
if not assets_dir.is_dir():
|
|
59
|
+
return None
|
|
60
|
+
candidates: list[Path] = []
|
|
61
|
+
for path in assets_dir.rglob("*.asset"):
|
|
62
|
+
try:
|
|
63
|
+
head = path.read_text(encoding="utf-8", errors="replace")[:4096]
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
if all(sig in head for sig in CONFIG_SIGNATURE_FIELDS):
|
|
67
|
+
candidates.append(path)
|
|
68
|
+
if not candidates:
|
|
69
|
+
return None
|
|
70
|
+
# If multiple configs exist (some repos package more than one), prefer the
|
|
71
|
+
# one nearest the repo root by path-segment count, then by name for
|
|
72
|
+
# determinism. Multi-package repos are out of scope for this skill — the
|
|
73
|
+
# user should pass --config-asset to disambiguate.
|
|
74
|
+
candidates.sort(key=lambda p: (len(p.relative_to(repo).parts), str(p)))
|
|
75
|
+
return candidates[0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_config_fields(asset_path: Path) -> dict[str, Any]:
|
|
79
|
+
"""Extract the top-level scalar fields of a PackageManifestConfig asset.
|
|
80
|
+
Returns a dict mapping field-name → {"value": str, "line": int} for every
|
|
81
|
+
top-level field encountered, and a flat dict of values under "_values" for
|
|
82
|
+
convenience. Skips list-element fields under `dependencies:`, `samples:`,
|
|
83
|
+
`copyEntries:`, etc., because they sit at four-space indent."""
|
|
84
|
+
lines = asset_path.read_text(encoding="utf-8").splitlines()
|
|
85
|
+
fields: dict[str, dict[str, Any]] = {}
|
|
86
|
+
values: dict[str, str] = {}
|
|
87
|
+
for idx, raw in enumerate(lines, start=1):
|
|
88
|
+
m = TOP_LEVEL_FIELD_RE.match(raw)
|
|
89
|
+
if not m:
|
|
90
|
+
continue
|
|
91
|
+
name = m.group("field")
|
|
92
|
+
val = m.group("value").strip()
|
|
93
|
+
# Re-occurrences at top level shadow earlier ones; the SO YAML doesn't
|
|
94
|
+
# normally do that for scalars, but be defensive.
|
|
95
|
+
fields[name] = {"value": val, "line": idx}
|
|
96
|
+
values[name] = val
|
|
97
|
+
return {"_fields": fields, "_values": values}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_repo_relative(repo: Path, value: str) -> Path:
|
|
101
|
+
"""Mirror the runtime path resolution that
|
|
102
|
+
`FileTools.CopyOrReplaceFileToDirectory` uses: `Path.GetFullPath(value)` is
|
|
103
|
+
resolved against the current working directory, which Unity sets to the
|
|
104
|
+
project root. So a relative path in the asset means "relative to repo
|
|
105
|
+
root"."""
|
|
106
|
+
p = Path(value)
|
|
107
|
+
if p.is_absolute():
|
|
108
|
+
return p
|
|
109
|
+
return (repo / p).resolve()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_last_changelog_heading(changelog_path: Path) -> dict[str, Any] | None:
|
|
113
|
+
"""Read the first `## ` heading from the changelog and try to extract the
|
|
114
|
+
version and the URL it links to, so the skill can mirror the same format
|
|
115
|
+
when prepending a new entry. The skill should not assume any particular URL
|
|
116
|
+
host — it's free-form text per repo."""
|
|
117
|
+
if not changelog_path.is_file():
|
|
118
|
+
return None
|
|
119
|
+
heading_re = re.compile(r"^##\s+(.*)$")
|
|
120
|
+
version_in_heading_re = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
|
121
|
+
plain_version_re = re.compile(r"\[?([0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.+-]+)?)\]?")
|
|
122
|
+
try:
|
|
123
|
+
text = changelog_path.read_text(encoding="utf-8")
|
|
124
|
+
except OSError:
|
|
125
|
+
return None
|
|
126
|
+
for line in text.splitlines():
|
|
127
|
+
m = heading_re.match(line)
|
|
128
|
+
if not m:
|
|
129
|
+
continue
|
|
130
|
+
body = m.group(1).strip()
|
|
131
|
+
linked = version_in_heading_re.search(body)
|
|
132
|
+
if linked:
|
|
133
|
+
version = linked.group(1).strip()
|
|
134
|
+
url = linked.group(2).strip()
|
|
135
|
+
tag_url_format = url.replace(version, "{version}", 1) if version and version in url else None
|
|
136
|
+
return {
|
|
137
|
+
"heading": line.rstrip(),
|
|
138
|
+
"version": version,
|
|
139
|
+
"url": url,
|
|
140
|
+
"tag_url_format": tag_url_format,
|
|
141
|
+
}
|
|
142
|
+
# Heading without a markdown link — still useful for showing the user.
|
|
143
|
+
plain = plain_version_re.search(body)
|
|
144
|
+
return {
|
|
145
|
+
"heading": line.rstrip(),
|
|
146
|
+
"version": plain.group(1) if plain else None,
|
|
147
|
+
"url": None,
|
|
148
|
+
"tag_url_format": None,
|
|
149
|
+
}
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_readme_install_snippet(readme_path: Path, package_name: str) -> dict[str, Any] | None:
|
|
154
|
+
"""Find the consumer-install snippet line in the project-root README — the
|
|
155
|
+
one that looks like `"com.example.foo": "X.Y.Z"`. Returns the line number,
|
|
156
|
+
the version string, and the raw line so the skill can do a targeted rewrite
|
|
157
|
+
without touching anything else in the README. Returns None when the README
|
|
158
|
+
has no such snippet (some repos document installation differently)."""
|
|
159
|
+
if not readme_path.is_file():
|
|
160
|
+
return None
|
|
161
|
+
pattern = re.compile(r'"' + re.escape(package_name) + r'"\s*:\s*"([^"]+)"')
|
|
162
|
+
try:
|
|
163
|
+
text = readme_path.read_text(encoding="utf-8")
|
|
164
|
+
except OSError:
|
|
165
|
+
return None
|
|
166
|
+
for idx, line in enumerate(text.splitlines(), start=1):
|
|
167
|
+
m = pattern.search(line)
|
|
168
|
+
if m:
|
|
169
|
+
return {"line": idx, "version": m.group(1), "raw": line}
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def detect_publish_flow(repo: Path) -> dict[str, Any]:
|
|
174
|
+
"""Sniff out what publish flow this repo uses. The release skill prints a
|
|
175
|
+
different handoff block per flow, and the maintainer can override via the
|
|
176
|
+
`publish-flow=` skill arg. Detection is best-effort — when ambiguous, the
|
|
177
|
+
skill should ask the user rather than guessing."""
|
|
178
|
+
gitlab_ci = repo / ".gitlab-ci.yml"
|
|
179
|
+
github_workflows_dir = repo / ".github" / "workflows"
|
|
180
|
+
flow: dict[str, Any] = {
|
|
181
|
+
"gitlab_ci_present": gitlab_ci.is_file(),
|
|
182
|
+
"gitlab_ci_includes_elestrago_unity_npm": False,
|
|
183
|
+
"github_workflows_present": github_workflows_dir.is_dir() and any(
|
|
184
|
+
p.is_file() and p.suffix in {".yml", ".yaml"}
|
|
185
|
+
for p in github_workflows_dir.iterdir()
|
|
186
|
+
),
|
|
187
|
+
"github_workflow_files": [],
|
|
188
|
+
"detected_flow": "manual",
|
|
189
|
+
}
|
|
190
|
+
if gitlab_ci.is_file():
|
|
191
|
+
try:
|
|
192
|
+
ci_text = gitlab_ci.read_text(encoding="utf-8")
|
|
193
|
+
except OSError:
|
|
194
|
+
ci_text = ""
|
|
195
|
+
# The shared pipeline include is the signal the skill's original CI
|
|
196
|
+
# handoff block was written for. Any other GitLab CI setup we treat as
|
|
197
|
+
# "gitlab-generic" so the skill can degrade gracefully.
|
|
198
|
+
if "unity-package-npm-publish" in ci_text or "elestrago/ci-pipelines" in ci_text:
|
|
199
|
+
flow["gitlab_ci_includes_elestrago_unity_npm"] = True
|
|
200
|
+
if flow["github_workflows_present"]:
|
|
201
|
+
flow["github_workflow_files"] = sorted(
|
|
202
|
+
str(p.relative_to(repo)) for p in github_workflows_dir.iterdir()
|
|
203
|
+
if p.is_file() and p.suffix in {".yml", ".yaml"}
|
|
204
|
+
)
|
|
205
|
+
# Prioritise: the elestrago-CI signal is the most specific, then any other
|
|
206
|
+
# GitLab CI, then GitHub Actions, then manual.
|
|
207
|
+
if flow["gitlab_ci_includes_elestrago_unity_npm"]:
|
|
208
|
+
flow["detected_flow"] = "gitlab-elestrago-ci"
|
|
209
|
+
elif flow["gitlab_ci_present"]:
|
|
210
|
+
flow["detected_flow"] = "gitlab-generic"
|
|
211
|
+
elif flow["github_workflows_present"]:
|
|
212
|
+
flow["detected_flow"] = "github-actions"
|
|
213
|
+
else:
|
|
214
|
+
flow["detected_flow"] = "manual"
|
|
215
|
+
return flow
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def read_release_package_json(repo: Path, destination_path: str) -> dict[str, Any] | None:
|
|
219
|
+
"""Read the `package.json` inside the exported destination if it exists.
|
|
220
|
+
Some CI flows (notably gitlab-elestrago-ci) trigger publishing on a version
|
|
221
|
+
delta of this file, so the skill needs to know whether it exists and what
|
|
222
|
+
version it currently advertises — so it can warn when the user is about to
|
|
223
|
+
skip the Export Package Source step and silently break the publish."""
|
|
224
|
+
if not destination_path:
|
|
225
|
+
return None
|
|
226
|
+
dest = resolve_repo_relative(repo, destination_path)
|
|
227
|
+
package_json = dest / "package.json"
|
|
228
|
+
if not package_json.is_file():
|
|
229
|
+
return {
|
|
230
|
+
"path": str(package_json.relative_to(repo)) if dest.is_relative_to(repo) else str(package_json),
|
|
231
|
+
"present": False,
|
|
232
|
+
"version": None,
|
|
233
|
+
}
|
|
234
|
+
try:
|
|
235
|
+
import json as _json
|
|
236
|
+
data = _json.loads(package_json.read_text(encoding="utf-8"))
|
|
237
|
+
version = data.get("version") if isinstance(data, dict) else None
|
|
238
|
+
except (OSError, ValueError):
|
|
239
|
+
version = None
|
|
240
|
+
return {
|
|
241
|
+
"path": str(package_json.relative_to(repo)) if dest.is_relative_to(repo) else str(package_json),
|
|
242
|
+
"present": True,
|
|
243
|
+
"version": version,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def read_git_state(repo: Path) -> dict[str, Any]:
|
|
248
|
+
"""Best-effort git state. Failures (missing git, not a repo) reduce to
|
|
249
|
+
empty/null fields rather than aborting — the skill can still do its job
|
|
250
|
+
without git, just without the convenience signals."""
|
|
251
|
+
def _run(args: list[str]) -> str | None:
|
|
252
|
+
try:
|
|
253
|
+
out = subprocess.check_output(
|
|
254
|
+
args, cwd=str(repo), stderr=subprocess.DEVNULL, text=True
|
|
255
|
+
)
|
|
256
|
+
return out.strip()
|
|
257
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
258
|
+
return None
|
|
259
|
+
return {
|
|
260
|
+
"branch": _run(["git", "rev-parse", "--abbrev-ref", "HEAD"]),
|
|
261
|
+
"remote_url": _run(["git", "remote", "get-url", "origin"]),
|
|
262
|
+
"recent_commits": _run(["git", "log", "--oneline", "-5"]),
|
|
263
|
+
"status_porcelain": _run(["git", "status", "--porcelain"]),
|
|
264
|
+
"diff_stat_head": _run(["git", "diff", "--stat", "HEAD"]),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def discover(repo: Path, override_config: Path | None = None) -> dict[str, Any]:
|
|
269
|
+
asset = override_config if override_config else find_config_asset(repo)
|
|
270
|
+
if asset is None or not asset.is_file():
|
|
271
|
+
return {
|
|
272
|
+
"ok": False,
|
|
273
|
+
"error": (
|
|
274
|
+
"No PackageManifestConfig.asset found under Assets/. This skill "
|
|
275
|
+
"requires the com.elestrago.unity.package-tools editor tool to be "
|
|
276
|
+
"installed in the project (it provides the ScriptableObject this "
|
|
277
|
+
"skill drives)."
|
|
278
|
+
),
|
|
279
|
+
}
|
|
280
|
+
parsed = parse_config_fields(asset)
|
|
281
|
+
values = parsed["_values"]
|
|
282
|
+
fields = parsed["_fields"]
|
|
283
|
+
|
|
284
|
+
package_name = values.get("packageName", "")
|
|
285
|
+
package_version = values.get("packageVersion")
|
|
286
|
+
changelog_path_raw = values.get("changelogPath", "")
|
|
287
|
+
readme_path_raw = values.get("readmePath", "")
|
|
288
|
+
license_path_raw = values.get("licensePath", "")
|
|
289
|
+
documentation_path_raw = values.get("documentationPath", "")
|
|
290
|
+
destination_path_raw = values.get("packageDestinationPath", "")
|
|
291
|
+
|
|
292
|
+
changelog_resolved = resolve_repo_relative(repo, changelog_path_raw) if changelog_path_raw else None
|
|
293
|
+
readme_resolved = resolve_repo_relative(repo, readme_path_raw) if readme_path_raw else None
|
|
294
|
+
|
|
295
|
+
changelog_info = read_last_changelog_heading(changelog_resolved) if changelog_resolved else None
|
|
296
|
+
readme_snippet = (
|
|
297
|
+
find_readme_install_snippet(readme_resolved, package_name)
|
|
298
|
+
if readme_resolved and package_name else None
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
release_json_info = read_release_package_json(repo, destination_path_raw)
|
|
302
|
+
publish_flow = detect_publish_flow(repo)
|
|
303
|
+
git_state = read_git_state(repo)
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"ok": True,
|
|
307
|
+
"repo": str(repo),
|
|
308
|
+
"config_asset": {
|
|
309
|
+
"path": str(asset.relative_to(repo)) if asset.is_relative_to(repo) else str(asset),
|
|
310
|
+
"package_version_line": fields.get("packageVersion", {}).get("line"),
|
|
311
|
+
},
|
|
312
|
+
"package": {
|
|
313
|
+
"name": package_name,
|
|
314
|
+
"display_name": values.get("displayName"),
|
|
315
|
+
"version": package_version,
|
|
316
|
+
"unity_version": values.get("unityVersion"),
|
|
317
|
+
"documentation_path": documentation_path_raw or None,
|
|
318
|
+
"destination_path": destination_path_raw or None,
|
|
319
|
+
},
|
|
320
|
+
"changelog": {
|
|
321
|
+
"path_in_config": changelog_path_raw or None,
|
|
322
|
+
"resolved_path": str(changelog_resolved.relative_to(repo)) if changelog_resolved and changelog_resolved.is_relative_to(repo) else (str(changelog_resolved) if changelog_resolved else None),
|
|
323
|
+
"exists": bool(changelog_resolved and changelog_resolved.is_file()),
|
|
324
|
+
"latest_heading": changelog_info,
|
|
325
|
+
},
|
|
326
|
+
"readme": {
|
|
327
|
+
"path_in_config": readme_path_raw or None,
|
|
328
|
+
"resolved_path": str(readme_resolved.relative_to(repo)) if readme_resolved and readme_resolved.is_relative_to(repo) else (str(readme_resolved) if readme_resolved else None),
|
|
329
|
+
"exists": bool(readme_resolved and readme_resolved.is_file()),
|
|
330
|
+
"install_snippet": readme_snippet,
|
|
331
|
+
},
|
|
332
|
+
"license": {
|
|
333
|
+
"path_in_config": license_path_raw or None,
|
|
334
|
+
},
|
|
335
|
+
"release_package_json": release_json_info,
|
|
336
|
+
"publish_flow": publish_flow,
|
|
337
|
+
"git": git_state,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main() -> int:
|
|
342
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
343
|
+
parser.add_argument(
|
|
344
|
+
"--repo",
|
|
345
|
+
default=os.getcwd(),
|
|
346
|
+
help="Repository root (defaults to current working directory).",
|
|
347
|
+
)
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"--config-asset",
|
|
350
|
+
default=None,
|
|
351
|
+
help=(
|
|
352
|
+
"Explicit path to PackageManifestConfig.asset. Use to disambiguate "
|
|
353
|
+
"in repos that ship multiple configs."
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
args = parser.parse_args()
|
|
357
|
+
repo = Path(args.repo).resolve()
|
|
358
|
+
override = Path(args.config_asset).resolve() if args.config_asset else None
|
|
359
|
+
result = discover(repo, override)
|
|
360
|
+
json.dump(result, sys.stdout, indent=2)
|
|
361
|
+
sys.stdout.write("\n")
|
|
362
|
+
return 0 if result.get("ok") else 2
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
sys.exit(main())
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.elestrago.unity.package-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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.
|
|
10
|
-
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.
|
|
11
|
-
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.
|
|
9
|
+
"documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.5.1/README.md",
|
|
10
|
+
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.5.1/CHANGELOG.md",
|
|
11
|
+
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.5.1/LICENSE",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"unity",
|
|
@@ -1,107 +0,0 @@
|
|
|
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/ExamplePrefab.prefab
|
|
12
|
-
└── Scene/SampleScene.unity
|
|
13
|
-
|
|
14
|
-
Assets/Samples~/ClaudeSkills
|
|
15
|
-
├── Editor/ClaudeSkillsPostImport.cs
|
|
16
|
-
├── Editor/Playdarium.PackageTool.Samples.ClaudeSkills.Editor.asmdef
|
|
17
|
-
├── unity-package-docs/SKILL.md
|
|
18
|
-
├── unity-package-docs/assets/README.md.template
|
|
19
|
-
├── unity-package-docs/assets/api-chunk.md.template
|
|
20
|
-
├── unity-package-docs/assets/api-index.md.template
|
|
21
|
-
├── unity-package-docs/assets/api.md.template
|
|
22
|
-
├── unity-package-docs/assets/manual.md.template
|
|
23
|
-
├── unity-package-docs/assets/samples.md.template
|
|
24
|
-
└── unity-package-docs/scripts/scan_package.py
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
On export, `CopySamplesToDirectory` copies each sample root to `Release/Samples~/<folderName>/`, skipping `.meta` files. The shipped samples are consumable from the Package Manager **Samples** tab on the published package.
|
|
28
|
-
|
|
29
|
-
## Sample -> Package Type Mapping
|
|
30
|
-
|
|
31
|
-
### `Assets/Example/Sample/Prefabs/ExamplePrefab.prefab`
|
|
32
|
-
|
|
33
|
-
Kind: prefab
|
|
34
|
-
|
|
35
|
-
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.
|
|
36
|
-
|
|
37
|
-
### `Assets/Example/Sample/Scene/SampleScene.unity`
|
|
38
|
-
|
|
39
|
-
Kind: scene
|
|
40
|
-
|
|
41
|
-
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.
|
|
42
|
-
|
|
43
|
-
### `Assets/Samples~/ClaudeSkills/Editor/ClaudeSkillsPostImport.cs`
|
|
44
|
-
|
|
45
|
-
Kind: script
|
|
46
|
-
|
|
47
|
-
Editor-side post-import hook bundled with the **Claude Skills** sample. In a consumer project, after the user imports this package's Claude Skills sample, the script copies the staged `unity-package-docs/` skill folder out of the imported `Assets/Samples/<Package>/<Version>/ClaudeSkills/` location into the consumer project's `.claude/skills/` directory, then self-deletes (and cleans up empty ancestor folders) so subsequent imports do not duplicate work. It does not call into the package's runtime API — it is plumbing for the sample's primary content (`SKILL.md`, `scripts/scan_package.py`, and the `.md.template` files under `assets/`).
|
|
48
|
-
|
|
49
|
-
Non-script assets that ship alongside this script (not enumerated above because the scanner only emits scripts/scenes/prefabs):
|
|
50
|
-
|
|
51
|
-
- `Assets/Samples~/ClaudeSkills/unity-package-docs/SKILL.md` — the skill definition this sample exists to deliver.
|
|
52
|
-
- `Assets/Samples~/ClaudeSkills/unity-package-docs/scripts/scan_package.py` — the scanner the skill invokes.
|
|
53
|
-
- `Assets/Samples~/ClaudeSkills/unity-package-docs/assets/*.md.template` — the rendering templates.
|
|
54
|
-
|
|
55
|
-
## Reproducing the Sample
|
|
56
|
-
|
|
57
|
-
**Example Sample (`Assets/Example/Sample`)** — structural only, no MonoBehaviour scripts to instantiate. To reproduce from scratch:
|
|
58
|
-
|
|
59
|
-
1. Create an empty Unity scene and a primitive prefab; both can live anywhere in `Assets/`.
|
|
60
|
-
2. Create a `PackageManifestConfig` asset via **Assets > Create > JCMG/PackageTools/PackageManifestConfig** and configure `sourcePath`, `packageDestinationPath`, and a [`Sample`](api.md#sample) entry whose `sourcePath` points at the folder holding the scene and prefab.
|
|
61
|
-
3. Click **Export Package Source** on the inspector — [`FileTools.CreateOrUpdatePackageSource`](api.md#filetools) runs the export pipeline (see `manual.md` -> **Export pipeline**) and the sample folder lands under `packageDestinationPath/Samples~/{folderName}`.
|
|
62
|
-
|
|
63
|
-
**Claude Skills (`Assets/Samples~/ClaudeSkills`)** — pairs a Sample with a [`CopyEntry`](api.md#packagemanifestconfigcopyentry) so the skill's canonical source lives in `.claude/skills/unity-package-docs/` (where it is editable and used directly during development) and a synchronized copy is staged into `Assets/Samples~/ClaudeSkills/unity-package-docs/` before export. The Editor-side `ClaudeSkillsPostImport.cs` script bundled with the sample handles the reverse direction in consumer projects: on import it moves the skill folder out of `Assets/Samples/<Package>/<Version>/ClaudeSkills/unity-package-docs/` into the consumer's `.claude/skills/`.
|
|
64
|
-
|
|
65
|
-
Minimal equivalent in C# for either sample style:
|
|
66
|
-
|
|
67
|
-
```csharp
|
|
68
|
-
using PackageTool;
|
|
69
|
-
using PackageTool.Tools;
|
|
70
|
-
using UnityEditor;
|
|
71
|
-
using UnityEngine;
|
|
72
|
-
|
|
73
|
-
var config = ScriptableObject.CreateInstance<PackageManifestConfig>();
|
|
74
|
-
config.packageName = "com.example.sample";
|
|
75
|
-
config.displayName = "Example Sample Package";
|
|
76
|
-
config.packageVersion = "1.0.0";
|
|
77
|
-
config.unityVersion = "2021.3";
|
|
78
|
-
config.sourcePath = "Assets/Example/Source";
|
|
79
|
-
config.packageDestinationPath = "Release";
|
|
80
|
-
config.samples = new[]
|
|
81
|
-
{
|
|
82
|
-
new PackageManifestConfig.Sample
|
|
83
|
-
{
|
|
84
|
-
sourcePath = "Assets/Example/Sample",
|
|
85
|
-
displayName = "Example Sample",
|
|
86
|
-
folderName = "ExampleSample",
|
|
87
|
-
description = "This is example for check test samples",
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
// Optional: stage external content into a Sample's sourcePath first.
|
|
91
|
-
config.copyEntries = new[]
|
|
92
|
-
{
|
|
93
|
-
new PackageManifestConfig.CopyEntry
|
|
94
|
-
{
|
|
95
|
-
sourcePath = ".claude/skills/unity-package-docs",
|
|
96
|
-
destinationPath = "Assets/Samples~/ClaudeSkills/unity-package-docs",
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
AssetDatabase.CreateAsset(config, "Assets/Example/PackageManifestConfig.asset");
|
|
100
|
-
FileTools.CreateOrUpdatePackageSource(config);
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Notes
|
|
104
|
-
|
|
105
|
-
- 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.
|
|
106
|
-
- 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.
|
|
107
|
-
- The `ClaudeSkillsPostImport` editor script only runs in consumer projects after they import the **Claude Skills** sample; it self-deletes after copying so a re-import does not duplicate work. It is not part of the package's primary API and is intentionally outside the `PackageTool` namespace.
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<!-- generated by unity-package-docs; safe to regenerate -->
|
|
2
|
-
|
|
3
|
-
# {{displayName}} — 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
|
-
{{sampleTree}}
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Sample → Package Type Mapping
|
|
14
|
-
|
|
15
|
-
{{sampleMappingSections}}
|
|
16
|
-
|
|
17
|
-
<!--
|
|
18
|
-
Each sample entry follows this exact shape:
|
|
19
|
-
|
|
20
|
-
### `{sample file path}`
|
|
21
|
-
|
|
22
|
-
Kind: script | scene | prefab
|
|
23
|
-
|
|
24
|
-
Package types referenced:
|
|
25
|
-
|
|
26
|
-
- [`{TypeName}`]({crossRefMap[TypeName]}) — `{package source path}:{line}`
|
|
27
|
-
|
|
28
|
-
For scenes/prefabs, list each MonoBehaviour script resolved via m_Script.guid:
|
|
29
|
-
|
|
30
|
-
- GameObject `{name}` uses [`{TypeName}`]({crossRefMap[TypeName]}) — `{package source path}`
|
|
31
|
-
|
|
32
|
-
Type links resolve through crossRefMap (Step 3.5), NOT a hard-coded
|
|
33
|
-
api.md anchor — the destination may be api.md#foo (api single-file) or
|
|
34
|
-
api/PackageTool.Tools.md#foo (api chunked).
|
|
35
|
-
|
|
36
|
-
Omit "Package types referenced" if the file references none from this package.
|
|
37
|
-
|
|
38
|
-
Chunking note (Step 6 chunked branch): when this file is split, entries
|
|
39
|
-
are grouped by top-level sample folder into Documentation~/samples/<SampleFolderName>.md
|
|
40
|
-
(filename sanitized to [A-Za-z0-9._-], non-matching chars replaced with `-`).
|
|
41
|
-
Each chunk carries its own `## Reproducing the Sample` and `## Notes`
|
|
42
|
-
sections. Inside chunk files, every `[Type](…)` link prepends `../` to
|
|
43
|
-
the crossRefMap value.
|
|
44
|
-
-->
|
|
45
|
-
|
|
46
|
-
## Reproducing the Sample
|
|
47
|
-
|
|
48
|
-
{{reproductionProse}}
|
|
49
|
-
|
|
50
|
-
```csharp
|
|
51
|
-
{{reproductionSnippet}}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Notes
|
|
55
|
-
|
|
56
|
-
{{notesList}}
|