com.elestrago.unity.package-tools 2.0.11 → 2.1.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.
Files changed (27) hide show
  1. package/CAHNGELOG.md +8 -0
  2. package/Documentation~/api.md +502 -0
  3. package/Documentation~/manual.md +140 -0
  4. package/Documentation~/samples.md +73 -0
  5. package/Editor/Drawers/CopyEntryPropertyDrawer.cs +95 -0
  6. package/Editor/Drawers/CopyEntryPropertyDrawer.cs.meta +2 -0
  7. package/Editor/EditorConstants.cs +6 -0
  8. package/Editor/Inspectors/PackageManifestConfigInspector.cs +31 -0
  9. package/Editor/PackageManifestConfig.cs +20 -0
  10. package/Editor/Tools/FileTools.cs +73 -0
  11. package/Samples~/ClaudeSkills/unity-package-docs/SKILL.md +309 -0
  12. package/Samples~/ClaudeSkills/unity-package-docs/assets/README.md.template +42 -0
  13. package/Samples~/ClaudeSkills/unity-package-docs/assets/api-chunk.md.template +41 -0
  14. package/Samples~/ClaudeSkills/unity-package-docs/assets/api-index.md.template +26 -0
  15. package/Samples~/ClaudeSkills/unity-package-docs/assets/api.md.template +43 -0
  16. package/Samples~/ClaudeSkills/unity-package-docs/assets/manual.md.template +57 -0
  17. package/Samples~/ClaudeSkills/unity-package-docs/assets/samples.md.template +56 -0
  18. package/Samples~/ClaudeSkills/unity-package-docs/scripts/scan_package.py +504 -0
  19. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/SKILL.md +309 -0
  20. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/README.md.template +42 -0
  21. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-chunk.md.template +41 -0
  22. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-index.md.template +26 -0
  23. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api.md.template +43 -0
  24. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/manual.md.template +57 -0
  25. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/samples.md.template +56 -0
  26. package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/scripts/scan_package.py +504 -0
  27. package/package.json +9 -4
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env python3
2
+ """Scan a Unity package source tree (and optional sample dirs) and emit a JSON manifest.
3
+
4
+ The manifest is consumed by the unity-package-docs skill to render README.md,
5
+ manual.md, api.md and samples.md. Paths in the manifest are relative to --repo-root
6
+ (default: cwd) so the consumer can use them verbatim in markdown.
7
+
8
+ Usage:
9
+ scan_package.py --package <pkg-dir> [--samples <dir> ...] [--repo-root <dir>]
10
+
11
+ The script uses only the Python standard library. It is best-effort regex-based C#
12
+ parsing: good enough for well-formatted Unity editor code, not a full Roslyn parser.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Iterable
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Source normalization: replace string and comment contents with whitespace so
27
+ # brace counts and regex matches are not fooled by braces in strings/comments.
28
+ # Preserves line numbers (newlines are kept verbatim).
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _blank_preserve_newlines(s: str) -> str:
32
+ return re.sub(r"[^\n]", " ", s)
33
+
34
+
35
+ def normalize(src: str) -> str:
36
+ out: list[str] = []
37
+ i, n = 0, len(src)
38
+ while i < n:
39
+ c = src[i]
40
+ c1 = src[i + 1] if i + 1 < n else ""
41
+ if c == "/" and c1 == "/":
42
+ j = src.find("\n", i)
43
+ j = n if j < 0 else j
44
+ out.append(" " * (j - i))
45
+ i = j
46
+ elif c == "/" and c1 == "*":
47
+ j = src.find("*/", i + 2)
48
+ j = n if j < 0 else j + 2
49
+ out.append(_blank_preserve_newlines(src[i:j]))
50
+ i = j
51
+ elif c == "@" and c1 == '"':
52
+ j = i + 2
53
+ while j < n:
54
+ if src[j] == '"':
55
+ if j + 1 < n and src[j + 1] == '"':
56
+ j += 2
57
+ continue
58
+ j += 1
59
+ break
60
+ j += 1
61
+ inner = src[i + 1 : j - 1] if j > i + 1 else ""
62
+ out.append('"' + _blank_preserve_newlines(inner) + '"')
63
+ i = j
64
+ elif c == '"':
65
+ j = i + 1
66
+ while j < n:
67
+ if src[j] == "\\" and j + 1 < n:
68
+ j += 2
69
+ continue
70
+ if src[j] == '"':
71
+ j += 1
72
+ break
73
+ if src[j] == "\n":
74
+ break
75
+ j += 1
76
+ chunk = src[i:j]
77
+ if len(chunk) >= 2 and chunk.endswith('"'):
78
+ out.append('"' + " " * (len(chunk) - 2) + '"')
79
+ else:
80
+ out.append(" " * len(chunk))
81
+ i = j
82
+ elif c == "'":
83
+ j = i + 1
84
+ depth = 0
85
+ while j < n and depth < 6:
86
+ if src[j] == "\\" and j + 1 < n:
87
+ j += 2
88
+ depth += 1
89
+ continue
90
+ if src[j] == "'":
91
+ j += 1
92
+ break
93
+ j += 1
94
+ depth += 1
95
+ chunk = src[i:j]
96
+ if len(chunk) >= 2 and chunk.endswith("'"):
97
+ out.append("'" + " " * (len(chunk) - 2) + "'")
98
+ else:
99
+ out.append(chunk)
100
+ i = j
101
+ else:
102
+ out.append(c)
103
+ i += 1
104
+ return "".join(out)
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Regex patterns applied to the normalized source line-by-line.
109
+ # ---------------------------------------------------------------------------
110
+
111
+ NAMESPACE_RE = re.compile(r"^\s*namespace\s+(?P<name>[\w.]+)\s*[{;]?\s*$")
112
+
113
+ TYPE_DECL_RE = re.compile(
114
+ r"^\s*(?P<mods>(?:(?:public|internal|private|protected|sealed|static|abstract|partial|new|unsafe)\s+)*)"
115
+ r"(?P<kind>class|struct|interface|enum)\s+"
116
+ r"(?P<name>[A-Za-z_]\w*)"
117
+ r"(?P<generic><[^>]*>)?"
118
+ r"\s*(?::\s*(?P<base>[^{]+?))?"
119
+ r"\s*(?:where\s+[^{]+)?"
120
+ r"\s*\{?\s*$"
121
+ )
122
+
123
+ METHOD_RE = re.compile(
124
+ r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|virtual|override|sealed|abstract|async|extern|new|partial|unsafe)\s+)+)"
125
+ r"(?P<ret>(?:[\w\.\?\[\]]|<[^>]*>)+(?:\s*[\?\[\]])*)\s+"
126
+ r"(?P<name>[A-Za-z_]\w*)\s*"
127
+ r"(?P<generic><[^>]*>)?\s*"
128
+ r"\((?P<params>[^)]*)\)\s*"
129
+ r"(?:where\s+[^{;=]+)?"
130
+ r"\s*(?:[{;=]|=>)?\s*$"
131
+ )
132
+
133
+ PROPERTY_RE = re.compile(
134
+ r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|virtual|override|sealed|abstract|new|readonly)\s+)+)"
135
+ r"(?P<type>(?:[\w\.\?\[\]]|<[^>]*>)+)\s+"
136
+ r"(?P<name>[A-Za-z_]\w*)\s*"
137
+ r"(?:\{[^}]*\}|=>\s*[^;]+;)"
138
+ )
139
+
140
+ FIELD_RE = re.compile(
141
+ r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|readonly|const|new|volatile|unsafe)\s+)+)"
142
+ r"(?P<type>(?:[\w\.\?\[\]]|<[^>]*>)+)\s+"
143
+ r"(?P<name>[A-Za-z_]\w*)\s*"
144
+ r"(?:=\s*[^;]+)?;"
145
+ )
146
+
147
+ ATTR_LINE_RE = re.compile(r"^\s*\[(?P<body>.+)\]\s*$")
148
+ SUMMARY_LINE_RE = re.compile(r"^\s*///\s?(?P<text>.*)$")
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # C# file parser
153
+ # ---------------------------------------------------------------------------
154
+
155
+ def parse_cs_file(path: Path, rel_path: str) -> dict:
156
+ """Return {'types': [...], 'namespaces': set([...])} for one .cs file."""
157
+ raw = path.read_text(encoding="utf-8", errors="replace")
158
+ norm = normalize(raw)
159
+ norm_lines = norm.split("\n")
160
+ raw_lines = raw.split("\n")
161
+
162
+ types: list[dict] = []
163
+ namespaces: set[str] = set()
164
+
165
+ scope_stack: list[dict] = [] # entries: {"kind": "namespace"|"type", "name": str, "open_depth": int, "type_idx": int|None}
166
+ brace_depth = 0
167
+ pending_attrs: list[str] = []
168
+ pending_summary: list[str] = []
169
+
170
+ def current_namespace() -> str:
171
+ for s in reversed(scope_stack):
172
+ if s["kind"] == "namespace":
173
+ return s["name"]
174
+ return ""
175
+
176
+ def current_type_idx() -> int | None:
177
+ for s in reversed(scope_stack):
178
+ if s["kind"] == "type":
179
+ return s["type_idx"]
180
+ return None
181
+
182
+ def drain_attrs_and_summary():
183
+ attrs = list(pending_attrs)
184
+ summary = "\n".join(pending_summary).strip()
185
+ pending_attrs.clear()
186
+ pending_summary.clear()
187
+ return attrs, summary
188
+
189
+ for idx, line in enumerate(norm_lines):
190
+ raw_line = raw_lines[idx] if idx < len(raw_lines) else ""
191
+ stripped = line.strip()
192
+
193
+ # Capture XML summary from RAW line (the normalizer keeps the `///` prefix).
194
+ m = SUMMARY_LINE_RE.match(raw_line)
195
+ if m:
196
+ text = m.group("text")
197
+ # Drop the <summary>/</summary> envelope; keep inner text.
198
+ text = re.sub(r"</?summary>", "", text).strip()
199
+ if text:
200
+ pending_summary.append(text)
201
+ continue
202
+
203
+ # Capture attribute lines (allow multiple attrs separated by commas).
204
+ # Read the body from the RAW line so string literal contents are preserved.
205
+ m = ATTR_LINE_RE.match(raw_line)
206
+ if m:
207
+ body = m.group("body").strip()
208
+ pending_attrs.append(body)
209
+ # Attribute lines don't open/close braces; safe to continue.
210
+ continue
211
+
212
+ if not stripped:
213
+ # blank line breaks attribute/summary accumulation? keep summary, drop attrs only on declaration.
214
+ # Reset summary on blank lines to avoid stealing distant comments.
215
+ pending_summary.clear()
216
+ pending_attrs.clear()
217
+ continue
218
+
219
+ # Namespace declaration
220
+ m = NAMESPACE_RE.match(line)
221
+ if m:
222
+ name = m.group("name")
223
+ namespaces.add(name)
224
+ # If the line had a `{`, that opens a scope.
225
+ if "{" in line:
226
+ scope_stack.append({"kind": "namespace", "name": name, "open_depth": brace_depth + 1, "type_idx": None})
227
+ brace_depth += line.count("{") - line.count("}")
228
+ elif ";" in line:
229
+ # file-scoped namespace: applies until EOF; no brace opened.
230
+ scope_stack.append({"kind": "namespace", "name": name, "open_depth": 0, "type_idx": None})
231
+ else:
232
+ # brace likely on next line; record pending namespace.
233
+ scope_stack.append({"kind": "namespace", "name": name, "open_depth": brace_depth + 1, "type_idx": None})
234
+ # don't drain attrs/summary for namespace
235
+ continue
236
+
237
+ # Type declaration (top-level OR nested directly inside another type body)
238
+ m = TYPE_DECL_RE.match(line)
239
+ nested_ok = current_type_idx() is None or _is_direct_type_body(scope_stack, brace_depth)
240
+ if m and nested_ok and not (
241
+ re.search(r"\b(new|return|throw)\b", line[: m.start("kind")])
242
+ ):
243
+ attrs, summary = drain_attrs_and_summary()
244
+ mods = [w for w in m.group("mods").split() if w] if m.group("mods") else []
245
+ base = m.group("base").strip() if m.group("base") else None
246
+ base_list = [b.strip() for b in base.split(",")] if base else []
247
+ tinfo = {
248
+ "file": rel_path,
249
+ "line": idx + 1,
250
+ "namespace": current_namespace(),
251
+ "name": m.group("name"),
252
+ "kind": m.group("kind"),
253
+ "modifiers": mods,
254
+ "baseList": base_list,
255
+ "attributes": attrs,
256
+ "summary": summary,
257
+ "containingType": _innermost_type_name(scope_stack),
258
+ "members": [],
259
+ }
260
+ types.append(tinfo)
261
+ type_idx = len(types) - 1
262
+ # If brace opens on this line, push scope. Else expect next line.
263
+ has_open = "{" in line
264
+ if has_open:
265
+ scope_stack.append({"kind": "type", "name": tinfo["name"], "open_depth": brace_depth + 1, "type_idx": type_idx})
266
+ else:
267
+ scope_stack.append({"kind": "type", "name": tinfo["name"], "open_depth": brace_depth + 1, "type_idx": type_idx})
268
+ brace_depth += line.count("{") - line.count("}")
269
+ # Pop scopes whose open_depth now exceeds brace_depth (closed in same line)
270
+ while scope_stack and brace_depth < scope_stack[-1]["open_depth"] - 1:
271
+ scope_stack.pop()
272
+ continue
273
+
274
+ # Member declarations only if we are directly inside a type scope (not nested in a method).
275
+ ct_idx = current_type_idx()
276
+ if ct_idx is not None and _is_direct_type_body(scope_stack, brace_depth):
277
+ member = _try_member(line, idx + 1)
278
+ if member is not None:
279
+ attrs, summary = drain_attrs_and_summary()
280
+ member["attributes"] = attrs
281
+ member["summary"] = summary
282
+ types[ct_idx]["members"].append(member)
283
+
284
+ # Track brace depth changes for this line.
285
+ opens = line.count("{")
286
+ closes = line.count("}")
287
+ brace_depth += opens - closes
288
+
289
+ # Pop scopes whose brace went below their open depth.
290
+ while scope_stack and brace_depth < scope_stack[-1]["open_depth"]:
291
+ scope_stack.pop()
292
+ # Clear attrs that didn't attach to anything (this line ended a statement).
293
+ if opens or closes or stripped.endswith(";"):
294
+ pending_attrs.clear()
295
+ pending_summary.clear()
296
+
297
+ return {"types": types, "namespaces": sorted(namespaces)}
298
+
299
+
300
+ def _innermost_type_name(scope_stack: list[dict]) -> str | None:
301
+ for s in reversed(scope_stack):
302
+ if s["kind"] == "type":
303
+ return s["name"]
304
+ return None
305
+
306
+
307
+ def _is_direct_type_body(scope_stack: list[dict], brace_depth: int) -> bool:
308
+ """True iff brace_depth equals the open depth of the innermost type (i.e. we're directly in the type body, not nested deeper)."""
309
+ for s in reversed(scope_stack):
310
+ if s["kind"] == "type":
311
+ return brace_depth == s["open_depth"]
312
+ return False
313
+
314
+
315
+ def _try_member(line: str, line_no: int) -> dict | None:
316
+ """Detect a method/property/field declaration on this normalized line."""
317
+ m = METHOD_RE.match(line)
318
+ if m and m.group("ret").strip() not in ("class", "struct", "interface", "enum"):
319
+ return {
320
+ "line": line_no,
321
+ "kind": "method",
322
+ "name": m.group("name"),
323
+ "signature": _collapse_ws(line.split("{")[0].split(";")[0].split("=>")[0]).rstrip(),
324
+ }
325
+ m = PROPERTY_RE.match(line)
326
+ if m:
327
+ return {
328
+ "line": line_no,
329
+ "kind": "property",
330
+ "name": m.group("name"),
331
+ "signature": _collapse_ws(line.split("{")[0].split("=>")[0]).rstrip(),
332
+ }
333
+ m = FIELD_RE.match(line)
334
+ if m:
335
+ return {
336
+ "line": line_no,
337
+ "kind": "field",
338
+ "name": m.group("name"),
339
+ "signature": _collapse_ws(line.split("=")[0].split(";")[0]).rstrip(),
340
+ }
341
+ return None
342
+
343
+
344
+ def _collapse_ws(s: str) -> str:
345
+ return re.sub(r"\s+", " ", s).strip()
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # Asmdef + meta scanning
350
+ # ---------------------------------------------------------------------------
351
+
352
+ def scan_asmdefs(package_dir: Path, repo_root: Path) -> list[dict]:
353
+ results: list[dict] = []
354
+ for p in package_dir.rglob("*.asmdef"):
355
+ try:
356
+ data = json.loads(p.read_text(encoding="utf-8"))
357
+ except json.JSONDecodeError:
358
+ continue
359
+ results.append(
360
+ {
361
+ "path": _rel(p, repo_root),
362
+ "name": data.get("name"),
363
+ "rootNamespace": data.get("rootNamespace", ""),
364
+ "includePlatforms": data.get("includePlatforms", []),
365
+ "excludePlatforms": data.get("excludePlatforms", []),
366
+ "references": data.get("references", []),
367
+ "precompiledReferences": data.get("precompiledReferences", []),
368
+ "defineConstraints": data.get("defineConstraints", []),
369
+ "autoReferenced": data.get("autoReferenced", True),
370
+ }
371
+ )
372
+ return results
373
+
374
+
375
+ META_GUID_RE = re.compile(r"^guid:\s*([a-fA-F0-9]+)", re.MULTILINE)
376
+
377
+
378
+ def build_script_guid_map(repo_root: Path) -> dict[str, str]:
379
+ """Walk every *.cs.meta under Assets/ and return {guid: relative_cs_path}."""
380
+ mapping: dict[str, str] = {}
381
+ assets = repo_root / "Assets"
382
+ if not assets.is_dir():
383
+ return mapping
384
+ for meta in assets.rglob("*.cs.meta"):
385
+ try:
386
+ text = meta.read_text(encoding="utf-8", errors="replace")
387
+ except OSError:
388
+ continue
389
+ m = META_GUID_RE.search(text)
390
+ if not m:
391
+ continue
392
+ cs_path = meta.with_suffix("") # drop the .meta
393
+ if cs_path.exists():
394
+ mapping[m.group(1)] = _rel(cs_path, repo_root)
395
+ return mapping
396
+
397
+
398
+ SCRIPT_REF_RE = re.compile(r"m_Script:\s*\{[^}]*guid:\s*([a-fA-F0-9]+)")
399
+
400
+
401
+ def scan_sample_dir(sample_dir: Path, repo_root: Path, package_types: set[str], guid_map: dict[str, str], package_files: set[str]) -> list[dict]:
402
+ results: list[dict] = []
403
+ if not sample_dir.is_dir():
404
+ return results
405
+ for p in sorted(sample_dir.rglob("*")):
406
+ if not p.is_file():
407
+ continue
408
+ rel = _rel(p, repo_root)
409
+ suf = p.suffix.lower()
410
+ if suf == ".cs":
411
+ try:
412
+ src = p.read_text(encoding="utf-8", errors="replace")
413
+ except OSError:
414
+ continue
415
+ norm = normalize(src)
416
+ ns_match = NAMESPACE_RE.search(norm)
417
+ ns = ns_match.group("name") if ns_match else ""
418
+ words = set(re.findall(r"\b([A-Z][A-Za-z0-9_]*)\b", norm))
419
+ refs = sorted(words & package_types)
420
+ results.append({"path": rel, "kind": "script", "namespace": ns, "packageTypesReferenced": refs})
421
+ elif suf in (".unity", ".prefab"):
422
+ try:
423
+ text = p.read_text(encoding="utf-8", errors="replace")
424
+ except OSError:
425
+ continue
426
+ guids = sorted(set(SCRIPT_REF_RE.findall(text)))
427
+ refs = []
428
+ for g in guids:
429
+ resolved = guid_map.get(g)
430
+ if resolved and resolved in package_files:
431
+ refs.append({"guid": g, "resolvedTo": resolved})
432
+ results.append({"path": rel, "kind": "scene" if suf == ".unity" else "prefab", "scriptsReferenced": refs})
433
+ return results
434
+
435
+
436
+ # ---------------------------------------------------------------------------
437
+ # Utilities
438
+ # ---------------------------------------------------------------------------
439
+
440
+ def _rel(p: Path, root: Path) -> str:
441
+ try:
442
+ return str(p.resolve().relative_to(root.resolve())).replace(os.sep, "/")
443
+ except ValueError:
444
+ return str(p)
445
+
446
+
447
+ def iter_cs_files(root: Path) -> Iterable[Path]:
448
+ for p in sorted(root.rglob("*.cs")):
449
+ if p.is_file():
450
+ yield p
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Main
455
+ # ---------------------------------------------------------------------------
456
+
457
+ def main(argv: list[str]) -> int:
458
+ ap = argparse.ArgumentParser(description="Scan a Unity package and emit a JSON manifest.")
459
+ ap.add_argument("--package", required=True, help="Path to the package source directory.")
460
+ ap.add_argument("--samples", action="append", default=[], help="Path to a sample directory (repeatable).")
461
+ ap.add_argument("--repo-root", default=os.getcwd(), help="Repository root (paths in output are relative to this).")
462
+ args = ap.parse_args(argv)
463
+
464
+ repo_root = Path(args.repo_root).resolve()
465
+ package_dir = Path(args.package).resolve()
466
+ if not package_dir.is_dir():
467
+ print(f"error: package dir not found: {package_dir}", file=sys.stderr)
468
+ return 2
469
+
470
+ all_types: list[dict] = []
471
+ all_namespaces: set[str] = set()
472
+ package_files: set[str] = set()
473
+
474
+ for cs in iter_cs_files(package_dir):
475
+ rel = _rel(cs, repo_root)
476
+ package_files.add(rel)
477
+ result = parse_cs_file(cs, rel)
478
+ all_types.extend(result["types"])
479
+ all_namespaces.update(result["namespaces"])
480
+
481
+ asmdefs = scan_asmdefs(package_dir, repo_root)
482
+ guid_map = build_script_guid_map(repo_root)
483
+
484
+ package_type_names: set[str] = {t["name"] for t in all_types}
485
+
486
+ samples: list[dict] = []
487
+ for s in args.samples:
488
+ samples.extend(scan_sample_dir(Path(s).resolve(), repo_root, package_type_names, guid_map, package_files))
489
+
490
+ manifest = {
491
+ "packageDir": _rel(package_dir, repo_root),
492
+ "asmdefs": asmdefs,
493
+ "namespaces": sorted(all_namespaces),
494
+ "types": all_types,
495
+ "scriptGuidCount": len(guid_map),
496
+ "samples": samples,
497
+ }
498
+ json.dump(manifest, sys.stdout, indent=2)
499
+ sys.stdout.write("\n")
500
+ return 0
501
+
502
+
503
+ if __name__ == "__main__":
504
+ sys.exit(main(sys.argv[1:]))
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "com.elestrago.unity.package-tools",
3
- "version": "2.0.11",
3
+ "version": "2.1.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.0.11/README.md",
10
- "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.0.11/CHANGELOG.md",
11
- "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.0.11/LICENSE",
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",
12
12
  "license": "MIT",
13
13
  "keywords": [
14
14
  "unity",
@@ -24,6 +24,11 @@
24
24
  "displayName": "Example Sample",
25
25
  "description": "This is example for check test samples",
26
26
  "path": "Samples~/ExampleSample"
27
+ },
28
+ {
29
+ "displayName": "Claude Skills",
30
+ "description": "This file contain claude skill for create package documentation",
31
+ "path": "Samples~/ClaudeSkills"
27
32
  }
28
33
  ],
29
34
  "author": {