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.
@@ -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.0",
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.3.0/README.md",
10
- "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.3.0/CHANGELOG.md",
11
- "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.3.0/LICENSE",
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}}