← Commit history

PCM: the wiki page as a KiCad PCM repository. Sample library package (10 symbols, 10 footprints) under pcm/src, tools/build_pcm.py packer with schema validation, pcm/repository.json + packages.json + the 1.0.0 zip, kicad_pcm_add_repository / kicad_pcm_remove_repository, kicad_pcm_list updateAvailable, kicad_pcm_install update:true, docs/pcm.md

John Lauer ·9ea1ed32bc ·26d ago ·parent e824138
23 files changed +5093−51
README.md+1
@@ -51,6 +51,7 @@ How the dock card, the dashboard and this demo fit together: [docs/dashboard.md] | [Dashboard](docs/dashboard.md) | The dock card, the dashboard and how the demo fits in. | | [How the bridge compares](docs/comparison.md) | Feature matrix against every notable KiCad MCP server and AI tool, with the gaps on both sides. | | [The Rust port plan](docs/rust-port-plan.md) | One dependency-free binary: what moves to KiCad's API, what ab already does, what PowerShell goes away, phases and risks. |+| [Your wiki page as a KiCad PCM repository](docs/pcm.md) | The page serves `repository.json`, `packages.json` and the zips anonymously, so it is a Plugin and Content Manager repository as-is: the sample library package, the packer, adding the repository in KiCad or with `kicad_pcm_add_repository`, and updates through PCM's Update button or `kicad_pcm_install {update:true}`. |  ## Live routing (from 0.9.340) 
docs/pcm.mdadded+229
@@ -0,0 +1,229 @@+# Your wiki page as a KiCad PCM repository++KiCad's Plugin and Content Manager (PCM) installs plugins, libraries and colour themes from+"repositories". A repository is not a server and not a protocol: it is three files reachable+by plain HTTPS GET. This page shows that an Adom wiki page already is one, with a sample+library package you can add to KiCad in a minute, and how the bridge drives the same+mechanism without the dialog.++Repository URL (paste it into KiCad or hand it to the verb below):++```+https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json+```++## What a PCM repository is++PCM's whole contract, from `kicad/pcm/schemas/pcm.v1.schema.json` on the KiCad 10.0 branch+and the addons guide at dev-docs.kicad.org:++| file | schema definition | what KiCad does with it |+|---|---|---|+| `repository.json` | `Repository` | The URL the user adds. Names the repository and points at `packages.json` with its `sha256` and an `update_timestamp`. PCM refetches `packages.json` only when the timestamp or the digest changes. |+| `packages.json` | `PackageArray` | Every package with every version: `download_url`, `download_sha256`, `download_size`, `install_size`, `status`, `kicad_version`. The newest compatible version above the installed one is what the Update button means. |+| `<identifier>-<version>.zip` | archive | `metadata.json` at the root (this one version only, no `download_*` keys, because a file cannot carry its own hash), then `symbols/`, `footprints/*.pretty/`, `3dmodels/`, `resources/icon.png`. PCM unpacks each folder under `${KICAD10_3RD_PARTY}/<folder>/<identifier_with_underscores>/`. |++The `identifier` is reverse-DNS (`com.adom.kicad-bridge.sample-lib`), every digest is+lower-case SHA-256, and KiCad fetches everything with an anonymous GET. There is no index+API, no login, no content negotiation. Anything that serves bytes at stable URLs is a PCM+repository.++## Why a wiki page is one naturally++Every wiki page is a git repository under the hood, and every file in it is served+anonymously at++```+https://wiki.adom.inc/api/pages/<owner>/<slug>/files/<path>+```++Verified for this page: `README.md` answers HTTP 200 with no session, and+`resources/adom-overlay-32.png` comes back byte-exact (same sha256 as the file in the repo).+So a push of `pcm/repository.json`, `pcm/packages.json` and `pcm/packages/*.zip` with+`adom-wiki repo push` is a PCM release, with the page's commit history as the changelog and+the same page holding the source of the package, the packer, and this guide. No bucket, no+CDN account, no second host.++## The exact layout++```+pcm/+  pcm.v1.schema.json                  KiCad's schema, vendored so the packer validates offline+  repository.json                     built: Repository+  packages.json                       built: PackageArray (every version still on disk)+  packages/+    com.adom.kicad-bridge.sample-lib-1.0.0.zip+    com.adom.kicad-bridge.sample-lib-1.1.0.zip   (after a bump)+  src/+    adom-sample-lib/                  the package source, one folder per package+      metadata.json                   Package: identifier, name, author, license, versions[]+      symbols/Adom_Sample.kicad_sym   ten symbols, AdomSample_R ... AdomSample_Battery_Cell+      footprints/Adom_Sample.pretty/  ten footprints, AdomSample_R_0402_1005Metric ... AdomSample_SOIC-8_3.9x4.9mm_P1.27mm+      resources/icon.png              64x64, shown in the PCM dialog+tools/build_pcm.py                    the packer+```++The sample symbols are copies of ten base symbols from KiCad 10's own `Device` library+(R, C, L, D, LED, C_Polarized, D_Zener, Fuse, Crystal, Battery_Cell) and the footprints are+copies from `Resistor_SMD`, `Capacitor_SMD`, `LED_SMD`, `Package_SO` and `Crystal`, all+renamed with an `AdomSample_` prefix so they never collide with the stock ones. The+footprints keep their `${KICAD10_3DMODEL_DIR}` model references, so they render in 3D with+KiCad's bundled bodies. After install, KiCad registers the libraries itself as+`PCM_Adom_Sample` (symbol and footprint tables) on its next start.++## The packer: `tools/build_pcm.py`++```bash+python3 tools/build_pcm.py                 # build the newest version listed in metadata.json+python3 tools/build_pcm.py --check         # validate only; exit 1 if packages.json would change+python3 tools/build_pcm.py --bump 1.1.0    # add 1.1.0 to metadata.json, build it, keep 1.0.0 listed+python3 tools/build_pcm.py --bump 1.1.0 --stamp   # same, and stamp AdomSample_R's Value with "R v1.1.0"+```++What it enforces, in order:++1. Source layout per the addons guide: `metadata.json` plus only the folders PCM extracts+   (`plugins`, `footprints`, `symbols`, `3dmodels`, `resources`, `colors`); anything else+   is refused. Each `.kicad_sym` and `.kicad_mod` is parsed quote-aware for balance, the+   footprint's declared name must equal its file stem, and `icon.png` must be a 64x64 PNG.+2. `metadata.json` validates as `Package`; `description` stays within the 150 characters+   the PCM dialog shows; no version may carry `download_*` keys (the packer computes them).+3. The archive: deterministic zip (fixed timestamps, sorted entries, `metadata.json` first)+   with only the version being packed in `versions[]` and no `download_*` keys. Same source+   in, same bytes out, so a second run rewrites nothing and the digests hold.+4. `packages.json`: every version in `metadata.json` whose zip is on disk, with+   `download_url` on this page, `download_sha256`, `download_size` and `install_size` read+   from the bytes. Validated as `PackageArray`. Versions accumulate; that is what makes+   PCM show Update.+5. `repository.json`: name, maintainer, `packages.url`, `packages.sha256`,+   `update_timestamp` (kept when `packages.json` did not change, bumped when it did, which+   is the signal PCM uses to refetch). Validated as `Repository`.++Validation runs a small draft-07 checker built into the packer and, when the `jsonschema`+module is importable, that library as a second opinion. Both read the vendored+`pcm/pcm.v1.schema.json`.++KiCad 10.0.2's own parser was also run over the sample: a board holding+`AdomSample_R_0805_2012Metric`, `AdomSample_SOIC-8_3.9x4.9mm_P1.27mm` and+`AdomSample_Crystal_SMD_3225-4Pin_3.2x2.5mm` passed `service-kicad pcb drc` with no+parse or geometry finding (only the expected "library Adom_Sample is not in the+configuration" note, since the service has no such library registered). The Rust test+`crates/kicad-core/tests/pcm_sample_lib.rs` parses all ten symbols and ten footprints with+the bridge's lint checks and unpacks the built zip with the bridge's own zip reader.++## Adding the repository to KiCad++Two ways to the same `kicad.json` row:++**In KiCad.** Plugin and Content Manager > Manage... > add (the + button) > paste the+repository URL above > OK. KiCad fetches `repository.json`, takes its name ("Adom KiCad+Bridge sample PCM") and lists the Adom Sample Library under Libraries.++**Through the bridge.**++```bash+adom-bridge kicad_pcm_add_repository '{"url":"https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json"}'+```++The verb fetches the `repository.json` to confirm it is one and to take its name (`name`+overrides; when the fetch fails and a name is given it registers unverified with a+warning), then appends `{"name": ..., "url": ...}` to `pcm.repositories` in+`<config>/kicad.json` exactly as KiCad writes it. It is idempotent on the URL, keeps a+`kicad.json.adom-bak` beside the file and replaces it atomically. When `kicad.json` had+no `pcm.repositories` key at all, KiCad's built-in "KiCad official repository" row is+written in front, so nothing the user had disappears. The honest part: KiCad reads the+list at start, so a running KiCad keeps its old list until relaunched, and a KiCad that is+running now will overwrite the edit when it saves its settings on exit. The response says+so (`restartNeeded: true`, `kicadRunning: null`). `kicad_pcm_remove_repository {url}` is+the reverse; installed packages stay installed.++## Installing++**In KiCad.** Plugin and Content Manager > Libraries tab > Adom Sample Library > Install >+Apply Pending Changes. Restart KiCad; `PCM_Adom_Sample` appears in the Symbol Editor and+the Footprint Editor.++**Through the bridge.**++```bash+adom-bridge kicad_pcm_install '{"repositoryUrl":"https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json","identifier":"com.adom.kicad-bridge.sample-lib"}'+adom-bridge kicad_pcm_list '{}'+```++`kicad_pcm_install` fetches `repository.json`, checks `packages.json` against its digest,+picks the newest version compatible with the detected KiCad (or `version`), checks the+zip against `download_sha256`, unpacks it under the 3rdparty root and writes the row in+`installed_packages.json` with `repository_id = sha256(url)[:16]`, which is why KiCad's own+PCM dialog then shows the package as Installed and can update or remove it.++`kicad_pcm_list` reads that registry, and for each package matches its `repository_id`+against the registered repositories, fetches that repository, and compares the newest+compatible version with the installed one: `updateAvailable`, `latestVersion`,+`updateCheck` (`ok`, `pinned`, `repository_not_registered`, `package_not_in_repository`,+`fetch_failed: ...`) per package and `updatesAvailable` in total. `checkUpdates: false`+skips the network.++## How updates flow++1. Change the package source. To make the change visible in the Symbol Editor, change a+   symbol's Value: `--stamp` does it for you by setting `AdomSample_R`'s Value from `R` to+   `R v1.1.0` (the Value text is drawn on the canvas next to the resistor body).+2. `python3 tools/build_pcm.py --bump 1.1.0 --stamp`. Now `packages.json` lists 1.0.0 and+   1.1.0, and `repository.json` carries a new digest and timestamp.+3. Push: `adom-wiki repo push adom/kicad-bridge --files pcm/src/adom-sample-lib/metadata.json pcm/src/adom-sample-lib/symbols/Adom_Sample.kicad_sym pcm/packages.json pcm/repository.json pcm/packages/com.adom.kicad-bridge.sample-lib-1.1.0.zip -m "sample-lib 1.1.0"`.+4. Either path sees it:+   - KiCad's PCM: restart KiCad, open Plugin and Content Manager; the Installed tab+     shows Update on Adom Sample Library (PCM refetches `packages.json` because+     `update_timestamp` moved). Update > Apply Pending Changes > restart.+   - The bridge: `kicad_pcm_list {}` reports `updateAvailable: true, latestVersion: "1.1.0"`;+     `kicad_pcm_install {"identifier":"com.adom.kicad-bridge.sample-lib","update":true}`+     resolves the repository from the installed row, removes the 1.0.0 directories, unpacks+     1.1.0 and rewrites the registry row (`previousVersion: "1.0.0", updated: true`). An+     installed package already at the newest version answers `upToDate: true`.++Either way the Symbol Editor shows `PCM_Adom_Sample` > `AdomSample_R` with the Value+`R v1.1.0` after the restart.++## Verb shapes++| verb | input | key output |+|---|---|---|+| `kicad_pcm_add_repository` | `{url, name?}` | `added`, `alreadyRegistered`, `name`, `repositoryId`, `verified`, `file`, `backup`, `repositories[]`, `restartNeeded` |+| `kicad_pcm_remove_repository` | `{url}` | `removed`, `file`, `backup`, `repositories[]` |+| `kicad_pcm_list` | `{checkUpdates?: true}` | per package `version`, `latestVersion`, `updateAvailable`, `updateCheck`, `repositoryUrl`, `onDisk`, `pinned`; `updatesAvailable`, `repositories[]` |+| `kicad_pcm_install` | `{repositoryUrl?, identifier, version?, update?, libPrefix?, force?}` | `version`, `previousVersion`, `updated`, `upToDate`, `alreadyInstalled`, `symbolLibraries`, `footprintLibraries`, `installedTo`, `restartNeeded` |+| `kicad_pcm_uninstall` | `{identifier}` | `removedDirs`, `recordRemoved`, `restartNeeded` |++Every write verb reports `kicadRunning: null` and `restartNeeded: true` on the native build:+it cannot yet tell whether KiCad is running, and it never restarts KiCad for you.++## Things that bit, so you do not have to++- `metadata.json` inside the zip must not carry `download_*`; PCM validates the archive's+  copy too. The packer strips them and refuses a source file that has them.+- PCM decides whether to refetch `packages.json` from `repository.json`'s+  `update_timestamp` and `sha256`. A repository whose timestamp never moves never shows an+  update. The packer bumps it exactly when `packages.json` changes.+- `install_size` counts the archive's `metadata.json`, which PCM does not extract; the+  on-disk footprint is a few KB smaller than the number. Cosmetic.+- KiCad registers `PCM_<library>` rows when the global library tables load, so libraries+  appear after a restart, never in a running editor. `pcm.lib_auto_add` off means no rows+  at all; the install response says so.+- A running KiCad overwrites `kicad.json` on exit. Add the repository with KiCad closed, or+  through KiCad's own dialog.+- `keep_on_update` patterns are honoured by KiCad's PCM and not by the bridge's update+  path (the sample has none).++## Proof on ConfRoomROG (KiCad 10.0.5)++Placeholders for the run; each line gets its output or a screenshot.++1. Repository served: `curl -s https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json` returns the JSON; the zip at `packages.json`'s `download_url` hashes to its `download_sha256`. Result: PENDING+2. `kicad_pcm_add_repository {url}` on ConfRoomROG: `added: true`, `kicad.json.adom-bak` present. Result: PENDING+3. `kicad_pcm_install {repositoryUrl, identifier}`: `version: "1.0.0"`, `symbolLibraries: ["PCM_Adom_Sample"]`, `footprintLibraries: ["PCM_Adom_Sample"]`. Result: PENDING+4. `kicad_pcm_list {}`: `updateAvailable: false`, `latestVersion: "1.0.0"`, `updateCheck: "ok"`. Result: PENDING+5. KiCad restarted; Plugin and Content Manager > Installed shows Adom Sample Library 1.0.0 from "Adom KiCad Bridge sample PCM"; Symbol Editor shows `PCM_Adom_Sample` with ten symbols. Screenshot: PENDING+6. `python3 tools/build_pcm.py --bump 1.1.0 --stamp`, push. `kicad_pcm_list {}`: `updateAvailable: true`, `latestVersion: "1.1.0"`. Result: PENDING+7. KiCad restarted; Plugin and Content Manager shows Update on Adom Sample Library (opened through the native menu). Screenshot: PENDING+8. `kicad_pcm_install {identifier, update: true}`: `previousVersion: "1.0.0"`, `version: "1.1.0"`, `updated: true`. Result: PENDING+9. KiCad restarted; Symbol Editor > `PCM_Adom_Sample` > `AdomSample_R` shows Value `R v1.1.0`. Screenshot: PENDING
pcm/packages.jsonadded+47
@@ -0,0 +1,47 @@+{+    "packages": [+        {+            "name": "Adom Sample Library",+            "description": "Ten symbols and ten footprints, served straight from an Adom wiki page acting as a KiCad PCM repository.",+            "description_full": "A small content library that proves an Adom wiki page can be a KiCad Plugin and Content Manager repository.\n\nSymbols (Adom_Sample.kicad_sym): AdomSample_R, AdomSample_C, AdomSample_L, AdomSample_D, AdomSample_LED, AdomSample_C_Polarized, AdomSample_D_Zener, AdomSample_Fuse, AdomSample_Crystal, AdomSample_Battery_Cell.\n\nFootprints (Adom_Sample.pretty): AdomSample_R_0402/0603/0805/1206, AdomSample_C_0402/0603/0805, AdomSample_LED_0603, AdomSample_SOIC-8, AdomSample_Crystal_SMD_3225.\n\nThe symbols and footprints are copies of KiCad's own Device, Resistor_SMD, Capacitor_SMD, LED_SMD, Package_SO and Crystal libraries, renamed with an AdomSample_ prefix so they never collide with the stock ones. KiCad registers them as PCM_Adom_Sample on its next start. The package, the repository.json and the packages.json are built by tools/build_pcm.py in the kicad-bridge repo and pushed to the wiki page; see docs/pcm.md there.",+            "identifier": "com.adom.kicad-bridge.sample-lib",+            "type": "library",+            "author": {+                "name": "Adom",+                "contact": {+                    "web": "https://adom.inc"+                }+            },+            "maintainer": {+                "name": "Adom",+                "contact": {+                    "web": "https://wiki.adom.inc/adom/kicad-bridge",+                    "email": "[email protected]"+                }+            },+            "license": "CC-BY-SA-4.0",+            "resources": {+                "homepage": "https://wiki.adom.inc/adom/kicad-bridge",+                "guide": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/docs/pcm.md",+                "source": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/src/adom-sample-lib/metadata.json"+            },+            "tags": [+                "adom",+                "sample",+                "symbols",+                "footprints"+            ],+            "versions": [+                {+                    "version": "1.0.0",+                    "status": "stable",+                    "kicad_version": "10.0",+                    "download_url": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/packages/com.adom.kicad-bridge.sample-lib-1.0.0.zip",+                    "download_sha256": "8a7e632acd3139936351c49b399bb52ba286041d04f9947b224307b164f18ca4",+                    "download_size": 13143,+                    "install_size": 51480+                }+            ]+        }+    ]+}
pcm/packages/com.adom.kicad-bridge.sample-lib-1.0.0.zipadded
⋯ 1 unchanged line ⋯
pcm/pcm.v1.schema.jsonadded+397
@@ -0,0 +1,397 @@+{+    "$schema": "http://json-schema.org/draft-07/schema#",+    "$id": "https://go.kicad.org/pcm/schemas/v1",+    "title": "KiCad PCM schema",+    "description": "KiCad Plugin and Content Manager repository and package metadata schema",+    "$ref": "#/definitions/Package",+    "definitions": {+        "Package": {+            "type": "object",+            "properties": {+                "name": {+                    "type": "string",+                    "description": "Package name",+                    "maxLength": 200+                },+                "description": {+                    "type": "string",+                    "description": "Short package description",+                    "maxLength": 500+                },+                "description_full": {+                    "type": "string",+                    "description": "Long package description",+                    "maxLength": 5000+                },+                "identifier": {+                    "type": "string",+                    "description": "Package identifier",+                    "pattern": "^[a-zA-Z][-a-zA-Z0-9.]{0,98}[a-zA-Z0-9]$"+                },+                "type": {+                    "type": "string",+                    "enum": [+                        "plugin",+                        "library",+                        "colortheme"+                    ],+                    "description": "Type of the package"+                },+                "author": {+                    "$ref": "#/definitions/Contact",+                    "description": "Author of the package"+                },+                "maintainer": {+                    "$ref": "#/definitions/Contact",+                    "description": "Maintainer of the package"+                },+                "category": {+                    "type": "string",+                    "description": "Category of the package"+                },+                "license": {+                    "$ref": "#/definitions/License",+                    "description": "Package distribution license"+                },+                "resources": {+                    "type": "object",+                    "description": "Additional resource links for the package",+                    "patternProperties": {+                        "^[a-zA-Z][-a-zA-Z0-9 ]{0,48}[a-zA-Z0-9]$": {+                            "type": "string",+                            "maxLength": 500+                        }+                    },+                    "additionalProperties": false+                },+                "tags": {+                    "type": "array",+                    "description": "Tags for the package",+                    "items": {+                        "type": "string",+                        "pattern": "^[a-z][-a-z0-9]{0,48}[a-z0-9]$"+                    },+                    "minItems": 1,+                    "uniqueItems": true+                },+                "keep_on_update": {+                    "type": "array",+                    "description": "List of regular expressions describing which files should be kept on package update",+                    "items": {+                        "type": "string"+                    },+                    "uniqueItems": true+                },+                "versions": {+                    "type": "array",+                    "description": "Package versions",+                    "items": {+                        "$ref": "#/definitions/PackageVersion"+                    },+                    "uniqueItems": true+                }+            },+            "required": [+                "name",+                "description",+                "description_full",+                "identifier",+                "type",+                "author",+                "license",+                "resources",+                "versions"+            ]+        },+        "Contact": {+            "type": "object",+            "description": "Contact information",+            "properties": {+                "name": {+                    "type": "string",+                    "description": "Contact's name",+                    "maxLength": 500+                },+                "contact": {+                    "type": "object",+                    "description": "Contact information details",+                    "patternProperties": {+                        "^[a-z][-a-z0-9 ]{0,48}[a-z0-9]$": {+                            "type": "string",+                            "maxLength": 500+                        }+                    },+                    "additionalProperties": false+                }+            },+            "required": [+                "name",+                "contact"+            ]+        },+        "PackageVersion": {+            "type": "object",+            "description": "Package version details",+            "properties": {+                "version": {+                    "type": "string",+                    "description": "Package version number major[.minor[.patch]]",+                    "pattern": "^\\d{1,4}(\\.\\d{1,4}(\\.\\d{1,6})?)?$"+                },+                "version_epoch": {+                    "type": "integer",+                    "description": "Version epoch number. Versions with higher epoch will be considered later.",+                    "minimum": 0+                },+                "download_url": {+                    "description": "URL for direct download of the package archive",+                    "$ref": "#/definitions/URL"+                },+                "download_sha256": {+                    "type": "string",+                    "description": "SHA-256 hash of the package archive as lower case hex string",+                    "pattern": "^[a-f0-9]{64}$"+                },+                "download_size": {+                    "description": "Download size in bytes",+                    "type": "integer",+                    "minimum": 0+                },+                "install_size": {+                    "description": "Extracted size in bytes",+                    "type": "integer",+                    "minimum": 0+                },+                "status": {+                    "type": "string",+                    "description": "Development status of the package version",+                    "enum": [+                        "stable",+                        "testing",+                        "development",+                        "deprecated"+                    ]+                },+                "platforms": {+                    "type": "array",+                    "description": "Supported platforms",+                    "items": {+                        "type": "string",+                        "enum": [+                            "windows",+                            "macos",+                            "linux"+                        ]+                    },+                    "minItems": 1,+                    "uniqueItems": true+                },+                "runtime": {+                    "type": "string",+                    "enum": [+                        "swig",+                        "ipc"+                    ],+                    "description": "What runtime a plugin requires.  Assumed to be swig if absent.  Not used for non-plugin packages."+                },+                "kicad_version": {+                    "type": "string",+                    "description": "Minimum supported KiCad version",+                    "pattern": "^\\d{1,2}(\\.\\d{1,2}(\\.\\d{1,2})?)?$"+                },+                "kicad_version_max": {+                    "type": "string",+                    "description": "Maximum supported KiCad version",+                    "pattern": "^\\d{1,2}(\\.\\d{1,2}(\\.\\d{1,2})?)?$"+                },+                "keep_on_update": {+                    "type": "array",+                    "description": "List of regular expressions describing which files should be kept on package update",+                    "items": {+                        "type": "string"+                    },+                    "uniqueItems": true+                }+            },+            "required": [+                "version",+                "status",+                "kicad_version"+            ]+        },+        "PackageArray": {+            "type": "object",+            "description": "Array of package descriptions",+            "properties": {+                "packages": {+                    "type": "array",+                    "items": {+                        "$ref": "#/definitions/Package"+                    }+                }+            },+            "required": [+                "packages"+            ]+        },+        "Repository": {+            "type": "object",+            "description": "Repository metadata",+            "properties": {+                "name": {+                    "type": "string",+                    "description": "Repository name",+                    "maxLength": 500+                },+                "packages": {+                    "description": "Reference to repository packages metadata",+                    "$ref": "#/definitions/RepositoryResource"+                },+                "resources": {+                    "description": "Reference to additional resource files archive",+                    "$ref": "#/definitions/RepositoryResource"+                },+                "manifests": {+                    "description": "Reference to package manifests",+                    "$ref": "#/definitions/RepositoryResource"+                },+                "maintainer": {+                    "description": "Repository maintainer",+                    "$ref": "#/definitions/Contact"+                },+                "$schema": {+                    "$ref": "#/definitions/URL"+                }+            },+            "required": [+                "name",+                "packages"+            ]+        },+        "RepositoryResource": {+            "type": "object",+            "properties": {+                "url": {+                    "$ref": "#/definitions/URL"+                },+                "sha256": {+                    "type": "string",+                    "description": "SHA-256 hash of the resource file as lower case hex string",+                    "pattern": "^[a-f0-9]{64}$"+                },+                "update_timestamp": {+                    "type": "integer",+                    "description": "Update timestamp of the resource file"+                },+                "update_time_utc": {+                    "type": "string",+                    "description": "Human readable update time in UTC YYYY-MM-DD HH:MM:SS format",+                    "pattern": "^2\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d$"+                }+            },+            "required": [+                "url",+                "update_timestamp"+            ]+        },+        "URL": {+            "type": "string",+            "pattern": "^(https?:\\/\\/[^\\s\\/$.?#].[^\\s]*)|(file:\\/\\/([a-zA-Z]:|\\/)[^\\x00]+)$"+        },+        "License": {+            "type": "string",+            "enum": [+                "public-domain",+                "Apache",+                "Apache-1.0",+                "Apache-2.0",+                "Artistic",+                "Artistic-1.0",+                "Artistic-2.0",+                "BSD",+                "BSD-2-Clause",+                "BSD-3-Clause",+                "BSD-4-Clause",+                "ISC",+                "CC-BY",+                "CC-BY-1.0",+                "CC-BY-2.0",+                "CC-BY-2.5",+                "CC-BY-3.0",+                "CC-BY-4.0",+                "CC-BY-SA",+                "CC-BY-SA-1.0",+                "CC-BY-SA-2.0",+                "CC-BY-SA-2.5",+                "CC-BY-SA-3.0",+                "CC-BY-SA-4.0",+                "CC-BY-ND",+                "CC-BY-ND-1.0",+                "CC-BY-ND-2.0",+                "CC-BY-ND-2.5",+                "CC-BY-ND-3.0",+                "CC-BY-ND-4.0",+                "CC-BY-NC",+                "CC-BY-NC-1.0",+                "CC-BY-NC-2.0",+                "CC-BY-NC-2.5",+                "CC-BY-NC-3.0",+                "CC-BY-NC-4.0",+                "CC-BY-NC-SA",+                "CC-BY-NC-SA-1.0",+                "CC-BY-NC-SA-2.0",+                "CC-BY-NC-SA-2.5",+                "CC-BY-NC-SA-3.0",+                "CC-BY-NC-SA-4.0",+                "CC-BY-NC-ND",+                "CC-BY-NC-ND-1.0",+                "CC-BY-NC-ND-2.0",+                "CC-BY-NC-ND-2.5",+                "CC-BY-NC-ND-3.0",+                "CC-BY-NC-ND-4.0",+                "CC0-1.0",+                "CDDL-1.0",+                "CPL",+                "EFL",+                "EFL-1.0",+                "EFL-2.0",+                "MIT",+                "GPL",+                "GPL-1.0",+                "GPL-2.0",+                "GPL-3.0",+                "LGPL",+                "LGPL-2.1",+                "LGPL-3.0",+                "GNU-LGPL-2.0",+                "GFDL",+                "GFDL-1.0",+                "GFDL-1.1",+                "GFDL-1.2",+                "GFDL-1.3",+                "GFDL-NIV",+                "LPPL",+                "LPPL-1.0",+                "LPPL-1.1",+                "LPPL-1.2",+                "LPPL-1.3",+                "MPL-1.1",+                "Perl",+                "Python-2.0",+                "QPL-1.0",+                "W3C",+                "Zlib",+                "Zope",+                "Zope-1.0",+                "Zope-1.1",+                "Zope-2.0",+                "Zope-2.1",+                "CERN-OHL",+                "WTFPL",+                "Unlicense",+                "open-source",+                "unrestricted"+            ]+        }+    }+}
pcm/repository.jsonadded+17
@@ -0,0 +1,17 @@+{+    "$schema": "https://go.kicad.org/pcm/schemas/v1",+    "name": "Adom KiCad Bridge sample PCM",+    "maintainer": {+        "name": "Adom",+        "contact": {+            "web": "https://wiki.adom.inc/adom/kicad-bridge",+            "email": "[email protected]"+        }+    },+    "packages": {+        "url": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/packages.json",+        "sha256": "6ad4fc7b7cec57ed8b77d31343e3c84e552ca9ca6bea63a56c289c5304daf256",+        "update_timestamp": 1789222441,+        "update_time_utc": "2026-09-12 14:14:01"+    }+}
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_C_0402_1005Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_C_0402_1005Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Capacitor SMD 0402 (1005 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 76, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "capacitor")+	(property "Reference" "REF**"+		(at 0 -1.16 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_C_0402_1005Metric"+		(at 0 1.16 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.107836 -0.36)+		(end 0.107836 -0.36)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.107836 0.36)+		(end 0.107836 0.36)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -0.91 -0.46)+		(end 0.91 0.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -0.5 -0.25)+		(end 0.5 0.25)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.25 0.25)+				(thickness 0.04)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.48 0)+		(size 0.56 0.62)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.48 0)+		(size 0.56 0.62)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0402_1005Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_C_0603_1608Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_C_0603_1608Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Capacitor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 76, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "capacitor")+	(property "Reference" "REF**"+		(at 0 -1.43 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_C_0603_1608Metric"+		(at 0 1.43 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.14058 -0.51)+		(end 0.14058 -0.51)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.14058 0.51)+		(end 0.14058 0.51)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -1.48 -0.73)+		(end 1.48 0.73)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -0.8 -0.4)+		(end 0.8 0.4)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.4 0.4)+				(thickness 0.06)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.775 0)+		(size 0.9 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.775 0)+		(size 0.9 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0603_1608Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_C_0805_2012Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_C_0805_2012Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Capacitor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 76, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf, https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing)")+	(tags "capacitor")+	(property "Reference" "REF**"+		(at 0 -1.68 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_C_0805_2012Metric"+		(at 0 1.68 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.261252 -0.735)+		(end 0.261252 -0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.261252 0.735)+		(end 0.261252 0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -1.7 -0.98)+		(end 1.7 0.98)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -1 -0.625)+		(end 1 0.625)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.5 0.5)+				(thickness 0.08)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.95 0)+		(size 1 1.45)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.95 0)+		(size 1 1.45)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Capacitor_SMD.3dshapes/C_0805_2012Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_Crystal_SMD_3225-4Pin_3.2x2.5mm.kicad_modadded+128
@@ -0,0 +1,128 @@+(footprint "AdomSample_Crystal_SMD_3225-4Pin_3.2x2.5mm"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "SMD3225/4, Crystal, 3.2x2.5mm package, SMD, http://www.txccrystal.com/images/pdf/7m-accuracy.pdf")+	(property "Reference" "REF**"+		(at 0 -2.45 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_Crystal_SMD_3225-4Pin_3.2x2.5mm"+		(at 0 2.45 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "crystal_resonator_oscillator"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -2.06 -1.71)+		(end -2.06 1.71)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -2.06 1.71)+		(end 2.06 1.71)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -2.1 -1.75)+		(end 2.1 1.75)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_poly+		(pts+			(xy 1.6 -1.25)+			(xy 1.6 1.25)+			(xy -0.975 1.25)+			(xy -1.6 0.625)+			(xy -1.6 -1.25)+		)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.8 0.8)+				(thickness 0.12)+			)+		)+	)+	(pad "1" smd roundrect+		(at -1.1 0.85)+		(size 1.4 1.2)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.208333)+	)+	(pad "2" smd roundrect+		(at 1.1 0.85)+		(size 1.4 1.2)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.208333)+	)+	(pad "3" smd roundrect+		(at 1.1 -0.85)+		(size 1.4 1.2)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.208333)+	)+	(pad "4" smd roundrect+		(at -1.1 -0.85)+		(size 1.4 1.2)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.208333)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Crystal.3dshapes/Crystal_SMD_3225-4Pin_3.2x2.5mm.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_LED_0603_1608Metric.kicad_modadded+156
@@ -0,0 +1,156 @@+(footprint "AdomSample_LED_0603_1608Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "LED SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf)")+	(tags "LED")+	(property "Reference" "REF**"+		(at 0 -1.43 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_LED_0603_1608Metric"+		(at 0 1.43 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -1.485 -0.735)+		(end -1.485 0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -1.485 0.735)+		(end 0.8 0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start 0.8 -0.735)+		(end -1.485 -0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -1.48 -0.73)+		(end 1.48 0.73)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -0.8 -0.1)+		(end -0.8 0.4)+		(stroke+			(width 0.1)+			(type solid)+		)+		(layer "F.Fab")+	)+	(fp_line+		(start -0.8 0.4)+		(end 0.8 0.4)+		(stroke+			(width 0.1)+			(type solid)+		)+		(layer "F.Fab")+	)+	(fp_line+		(start -0.5 -0.4)+		(end -0.8 -0.1)+		(stroke+			(width 0.1)+			(type solid)+		)+		(layer "F.Fab")+	)+	(fp_line+		(start 0.8 -0.4)+		(end -0.5 -0.4)+		(stroke+			(width 0.1)+			(type solid)+		)+		(layer "F.Fab")+	)+	(fp_line+		(start 0.8 0.4)+		(end 0.8 -0.4)+		(stroke+			(width 0.1)+			(type solid)+		)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.4 0.4)+				(thickness 0.06)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.7875 0)+		(size 0.875 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.7875 0)+		(size 0.875 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/LED_SMD.3dshapes/LED_0603_1608Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_R_0402_1005Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_R_0402_1005Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Resistor SMD 0402 (1005 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "resistor")+	(property "Reference" "REF**"+		(at 0 -1.17 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_R_0402_1005Metric"+		(at 0 1.17 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.153641 -0.38)+		(end 0.153641 -0.38)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.153641 0.38)+		(end 0.153641 0.38)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -0.93 -0.47)+		(end 0.93 0.47)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -0.525 -0.27)+		(end 0.525 0.27)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.26 0.26)+				(thickness 0.04)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.51 0)+		(size 0.54 0.64)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.51 0)+		(size 0.54 0.64)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0402_1005Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_R_0603_1608Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_R_0603_1608Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Resistor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "resistor")+	(property "Reference" "REF**"+		(at 0 -1.43 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_R_0603_1608Metric"+		(at 0 1.43 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.237258 -0.5225)+		(end 0.237258 -0.5225)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.237258 0.5225)+		(end 0.237258 0.5225)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -1.48 -0.73)+		(end 1.48 0.73)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -0.8 -0.4125)+		(end 0.8 0.4125)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.4 0.4)+				(thickness 0.06)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.825 0)+		(size 0.8 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at 0.825 0)+		(size 0.8 0.95)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0603_1608Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_R_0805_2012Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_R_0805_2012Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Resistor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "resistor")+	(property "Reference" "REF**"+		(at 0 -1.65 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_R_0805_2012Metric"+		(at 0 1.65 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.227064 -0.735)+		(end 0.227064 -0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.227064 0.735)+		(end 0.227064 0.735)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -1.68 -0.95)+		(end 1.68 0.95)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -1 -0.625)+		(end 1 0.625)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.5 0.5)+				(thickness 0.08)+			)+		)+	)+	(pad "1" smd roundrect+		(at -0.9125 0)+		(size 1.025 1.4)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.243902)+	)+	(pad "2" smd roundrect+		(at 0.9125 0)+		(size 1.025 1.4)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.243902)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0805_2012Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_R_1206_3216Metric.kicad_modadded+112
@@ -0,0 +1,112 @@+(footprint "AdomSample_R_1206_3216Metric"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "Resistor SMD 1206 (3216 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)")+	(tags "resistor")+	(property "Reference" "REF**"+		(at 0 -1.83 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_R_1206_3216Metric"+		(at 0 1.83 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "SMD_2terminal_chip_molded"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -0.727064 -0.91)+		(end 0.727064 -0.91)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -0.727064 0.91)+		(end 0.727064 0.91)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_rect+		(start -2.28 -1.13)+		(end 2.28 1.13)+		(stroke+			(width 0.05)+			(type solid)+		)+		(fill no)+		(layer "F.CrtYd")+	)+	(fp_rect+		(start -1.6 -0.8)+		(end 1.6 0.8)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 0)+		(layer "F.Fab")+		(effects+			(font+				(size 0.8 0.8)+				(thickness 0.12)+			)+		)+	)+	(pad "1" smd roundrect+		(at -1.4625 0)+		(size 1.125 1.75)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.222222)+	)+	(pad "2" smd roundrect+		(at 1.4625 0)+		(size 1.125 1.75)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.222222)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_1206_3216Metric.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty/AdomSample_SOIC-8_3.9x4.9mm_P1.27mm.kicad_modadded+300
@@ -0,0 +1,300 @@+(footprint "AdomSample_SOIC-8_3.9x4.9mm_P1.27mm"+	(version 20260206)+	(generator "kicad-footprint-generator")+	(layer "F.Cu")+	(descr "SOIC, 8 Pin (JEDEC MS-012AA, https://www.analog.com/media/en/package-pcb-resources/package/pkg_pdf/soic_narrow-r/r_8.pdf)")+	(tags "SOIC SO")+	(property "Reference" "REF**"+		(at 0 -3.4 0)+		(layer "F.SilkS")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "Value" "AdomSample_SOIC-8_3.9x4.9mm_P1.27mm"+		(at 0 3.4 0)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(property "KiLib_Generator" "package/gullwing"+		(at 0 0 0)+		(layer "F.SilkS")+		(hide yes)+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(attr smd)+	(duplicate_pad_numbers_are_jumpers no)+	(fp_line+		(start -2.06 -2.56)+		(end 2.06 -2.56)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -2.06 -2.465)+		(end -2.06 -2.56)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start -2.06 2.56)+		(end -2.06 2.465)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start 2.06 -2.56)+		(end 2.06 -2.465)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start 2.06 2.465)+		(end 2.06 2.56)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_line+		(start 2.06 2.56)+		(end -2.06 2.56)+		(stroke+			(width 0.12)+			(type solid)+		)+		(layer "F.SilkS")+	)+	(fp_poly+		(pts+			(xy -2.6 -2.47)+			(xy -2.84 -2.8)+			(xy -2.36 -2.8)+		)+		(stroke+			(width 0.12)+			(type solid)+		)+		(fill yes)+		(layer "F.SilkS")+	)+	(fp_line+		(start -3.7 -2.46)+		(end -2.2 -2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -3.7 2.46)+		(end -3.7 -2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -2.2 -2.7)+		(end 2.2 -2.7)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -2.2 -2.46)+		(end -2.2 -2.7)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -2.2 2.46)+		(end -3.7 2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start -2.2 2.7)+		(end -2.2 2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 2.2 -2.7)+		(end 2.2 -2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 2.2 -2.46)+		(end 3.7 -2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 2.2 2.46)+		(end 2.2 2.7)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 2.2 2.7)+		(end -2.2 2.7)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 3.7 -2.46)+		(end 3.7 2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_line+		(start 3.7 2.46)+		(end 2.2 2.46)+		(stroke+			(width 0.05)+			(type solid)+		)+		(layer "F.CrtYd")+	)+	(fp_poly+		(pts+			(xy -0.975 -2.45)+			(xy 1.95 -2.45)+			(xy 1.95 2.45)+			(xy -1.95 2.45)+			(xy -1.95 -1.475)+		)+		(stroke+			(width 0.1)+			(type solid)+		)+		(fill no)+		(layer "F.Fab")+	)+	(fp_text user "${REFERENCE}"+		(at 0 0 90)+		(layer "F.Fab")+		(effects+			(font+				(size 1 1)+				(thickness 0.15)+			)+		)+	)+	(pad "1" smd roundrect+		(at -2.475 -1.905)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "2" smd roundrect+		(at -2.475 -0.635)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "3" smd roundrect+		(at -2.475 0.635)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "4" smd roundrect+		(at -2.475 1.905)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "5" smd roundrect+		(at 2.475 1.905)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "6" smd roundrect+		(at 2.475 0.635)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "7" smd roundrect+		(at 2.475 -0.635)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(pad "8" smd roundrect+		(at 2.475 -1.905)+		(size 1.95 0.6)+		(layers "F.Cu" "F.Mask" "F.Paste")+		(roundrect_rratio 0.25)+	)+	(embedded_fonts no)+	(model "${KICAD10_3DMODEL_DIR}/Package_SO.3dshapes/SOIC-8_3.9x4.9mm_P1.27mm.step"+		(offset+			(xyz 0 0 0)+		)+		(scale+			(xyz 1 1 1)+		)+		(rotate+			(xyz 0 0 0)+		)+	)+)
pcm/src/adom-sample-lib/metadata.jsonadded+40
@@ -0,0 +1,40 @@+{+    "$schema": "https://go.kicad.org/pcm/schemas/v1",+    "name": "Adom Sample Library",+    "description": "Ten symbols and ten footprints, served straight from an Adom wiki page acting as a KiCad PCM repository.",+    "description_full": "A small content library that proves an Adom wiki page can be a KiCad Plugin and Content Manager repository.\n\nSymbols (Adom_Sample.kicad_sym): AdomSample_R, AdomSample_C, AdomSample_L, AdomSample_D, AdomSample_LED, AdomSample_C_Polarized, AdomSample_D_Zener, AdomSample_Fuse, AdomSample_Crystal, AdomSample_Battery_Cell.\n\nFootprints (Adom_Sample.pretty): AdomSample_R_0402/0603/0805/1206, AdomSample_C_0402/0603/0805, AdomSample_LED_0603, AdomSample_SOIC-8, AdomSample_Crystal_SMD_3225.\n\nThe symbols and footprints are copies of KiCad's own Device, Resistor_SMD, Capacitor_SMD, LED_SMD, Package_SO and Crystal libraries, renamed with an AdomSample_ prefix so they never collide with the stock ones. KiCad registers them as PCM_Adom_Sample on its next start. The package, the repository.json and the packages.json are built by tools/build_pcm.py in the kicad-bridge repo and pushed to the wiki page; see docs/pcm.md there.",+    "identifier": "com.adom.kicad-bridge.sample-lib",+    "type": "library",+    "author": {+        "name": "Adom",+        "contact": {+            "web": "https://adom.inc"+        }+    },+    "maintainer": {+        "name": "Adom",+        "contact": {+            "web": "https://wiki.adom.inc/adom/kicad-bridge",+            "email": "[email protected]"+        }+    },+    "license": "CC-BY-SA-4.0",+    "resources": {+        "homepage": "https://wiki.adom.inc/adom/kicad-bridge",+        "guide": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/docs/pcm.md",+        "source": "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/src/adom-sample-lib/metadata.json"+    },+    "tags": [+        "adom",+        "sample",+        "symbols",+        "footprints"+    ],+    "versions": [+        {+            "version": "1.0.0",+            "status": "stable",+            "kicad_version": "10.0"+        }+    ]+}
pcm/src/adom-sample-lib/resources/icon.pngadded
⋯ 1 unchanged line ⋯
pcm/src/adom-sample-lib/symbols/Adom_Sample.kicad_symadded+1775
@@ -0,0 +1,1775 @@+(kicad_symbol_lib+	(version 20251024)+	(generator "kicad_symbol_editor")+	(generator_version "10.0")+	(symbol "AdomSample_R"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 0)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "R"+			(at 2.032 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "R"+			(at 0 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at -1.778 0 90)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Resistor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "R res resistor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "R_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_R_0_1"+			(rectangle+				(start -1.016 -2.54)+				(end 1.016 2.54)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_R_1_1"+			(pin passive line+				(at 0 3.81 270)+				(length 1.27)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -3.81 90)+				(length 1.27)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_C"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 0.254)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "C"+			(at 0.635 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Value" "C"+			(at 0.635 -2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Footprint" ""+			(at 0.9652 -3.81 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Unpolarized capacitor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "cap capacitor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "C_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_C_0_1"+			(polyline+				(pts+					(xy -2.032 0.762) (xy 2.032 0.762)+				)+				(stroke+					(width 0.508)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -2.032 -0.762) (xy 2.032 -0.762)+				)+				(stroke+					(width 0.508)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_C_1_1"+			(pin passive line+				(at 0 3.81 270)+				(length 2.794)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -3.81 90)+				(length 2.794)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_L"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 1.016)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "L"+			(at -1.27 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "L"+			(at 1.905 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Inductor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "inductor choke coil reactor magnetic"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "Choke_* *Coil* Inductor_* L_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_L_0_1"+			(arc+				(start 0 2.54)+				(mid 0.6323 1.905)+				(end 0 1.27)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(arc+				(start 0 1.27)+				(mid 0.6323 0.635)+				(end 0 0)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(arc+				(start 0 0)+				(mid 0.6323 -0.635)+				(end 0 -1.27)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(arc+				(start 0 -1.27)+				(mid 0.6323 -1.905)+				(end 0 -2.54)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_L_1_1"+			(pin passive line+				(at 0 3.81 270)+				(length 1.27)+				(name "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -3.81 90)+				(length 1.27)+				(name "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_D"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 1.016)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "D"+			(at 0 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "D"+			(at 0 -2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Device" "D"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Pins" "1=K 2=A"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "TO-???* *_Diode_* *SingleDiode* D_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_D_0_1"+			(polyline+				(pts+					(xy -1.27 1.27) (xy -1.27 -1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 1.27) (xy 1.27 -1.27) (xy -1.27 0) (xy 1.27 1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 0) (xy -1.27 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_D_1_1"+			(pin passive line+				(at -3.81 0 0)+				(length 2.54)+				(name "K"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 3.81 0 180)+				(length 2.54)+				(name "A"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_LED"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 1.016)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "D"+			(at 0 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "LED"+			(at 0 -2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Light emitting diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Pins" "1=K 2=A"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "LED diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "LED* LED_SMD:* LED_THT:*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_LED_0_1"+			(polyline+				(pts+					(xy -3.048 -0.762) (xy -4.572 -2.286) (xy -3.81 -2.286) (xy -4.572 -2.286) (xy -4.572 -1.524)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.778 -0.762) (xy -3.302 -2.286) (xy -2.54 -2.286) (xy -3.302 -2.286) (xy -3.302 -1.524)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.27 0) (xy 1.27 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.27 -1.27) (xy -1.27 1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 -1.27) (xy 1.27 1.27) (xy -1.27 0) (xy 1.27 -1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_LED_1_1"+			(pin passive line+				(at -3.81 0 0)+				(length 2.54)+				(name "K"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 3.81 0 180)+				(length 2.54)+				(name "A"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_C_Polarized"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 0.254)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "C"+			(at 0.635 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Value" "C_Polarized"+			(at 0.635 -2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Footprint" ""+			(at 0.9652 -3.81 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Polarized capacitor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "cap capacitor"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "CP_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_C_Polarized_0_1"+			(rectangle+				(start -2.286 0.508)+				(end 2.286 1.016)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.778 2.286) (xy -0.762 2.286)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.27 2.794) (xy -1.27 1.778)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(rectangle+				(start 2.286 -0.508)+				(end -2.286 -1.016)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type outline)+				)+			)+		)+		(symbol "AdomSample_C_Polarized_1_1"+			(pin passive line+				(at 0 3.81 270)+				(length 2.794)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -3.81 90)+				(length 2.794)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_D_Zener"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 1.016)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "D"+			(at 0 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "D_Zener"+			(at 0 -2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Zener diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "diode"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "TO-???* *_Diode_* *SingleDiode* D_*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_D_Zener_0_1"+			(polyline+				(pts+					(xy -1.27 -1.27) (xy -1.27 1.27) (xy -0.762 1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 0) (xy -1.27 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 -1.27) (xy 1.27 1.27) (xy -1.27 0) (xy 1.27 -1.27)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_D_Zener_1_1"+			(pin passive line+				(at -3.81 0 0)+				(length 2.54)+				(name "K"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 3.81 0 180)+				(length 2.54)+				(name "A"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_Fuse"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 0)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "F"+			(at 2.032 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "Fuse"+			(at -1.905 0 90)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at -1.778 0 90)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Fuse"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "fuse"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "*Fuse*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_Fuse_0_1"+			(rectangle+				(start -0.762 -2.54)+				(end 0.762 2.54)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 0 2.54) (xy 0 -2.54)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_Fuse_1_1"+			(pin passive line+				(at 0 3.81 270)+				(length 1.27)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -3.81 90)+				(length 1.27)+				(name ""+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_Crystal"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 1.016)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "Y"+			(at 0 3.81 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Value" "Crystal"+			(at 0 -3.81 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Footprint" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Two pin crystal"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "quartz ceramic resonator oscillator"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_fp_filters" "Crystal*"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_Crystal_0_1"+			(polyline+				(pts+					(xy -2.54 0) (xy -1.905 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy -1.905 -1.27) (xy -1.905 1.27)+				)+				(stroke+					(width 0.508)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(rectangle+				(start -1.143 2.54)+				(end 1.143 -2.54)+				(stroke+					(width 0.3048)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.905 -1.27) (xy 1.905 1.27)+				)+				(stroke+					(width 0.508)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 2.54 0) (xy 1.905 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_Crystal_1_1"+			(pin passive line+				(at -3.81 0 0)+				(length 1.27)+				(name "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 3.81 0 180)+				(length 1.27)+				(name "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+	(symbol "AdomSample_Battery_Cell"+		(pin_numbers+			(hide yes)+		)+		(pin_names+			(offset 0)+			(hide yes)+		)+		(exclude_from_sim no)+		(in_bom yes)+		(on_board yes)+		(in_pos_files yes)+		(duplicate_pin_numbers_are_jumpers no)+		(property "Reference" "BT"+			(at 2.54 2.54 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Value" "Battery_Cell"+			(at 2.54 0 0)+			(show_name no)+			(do_not_autoplace no)+			(effects+				(font+					(size 1.27 1.27)+				)+				(justify left)+			)+		)+		(property "Footprint" ""+			(at 0 1.524 90)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Datasheet" ""+			(at 0 1.524 90)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Description" "Single-cell battery"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Device" "V"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Type" "DC"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "Sim.Pins" "1=+ 2=-"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(property "ki_keywords" "battery cell"+			(at 0 0 0)+			(show_name no)+			(do_not_autoplace no)+			(hide yes)+			(effects+				(font+					(size 1.27 1.27)+				)+			)+		)+		(symbol "AdomSample_Battery_Cell_0_1"+			(rectangle+				(start -2.286 1.778)+				(end 2.286 1.524)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type outline)+				)+			)+			(rectangle+				(start -1.524 1.016)+				(end 1.524 0.508)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type outline)+				)+			)+			(polyline+				(pts+					(xy 0 1.778) (xy 0 2.54)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 0 0.762) (xy 0 0)+				)+				(stroke+					(width 0)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 0.762 3.048) (xy 1.778 3.048)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+			(polyline+				(pts+					(xy 1.27 3.556) (xy 1.27 2.54)+				)+				(stroke+					(width 0.254)+					(type default)+				)+				(fill+					(type none)+				)+			)+		)+		(symbol "AdomSample_Battery_Cell_1_1"+			(pin passive line+				(at 0 5.08 270)+				(length 2.54)+				(name "+"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "1"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+			(pin passive line+				(at 0 -2.54 90)+				(length 2.54)+				(name "-"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+				(number "2"+					(effects+						(font+							(size 1.27 1.27)+						)+					)+				)+			)+		)+		(embedded_fonts no)+	)+)
rust/crates/kicad-bridge/src/verbs_maint.rs+43−13
@@ -1,5 +1,6 @@ //! Verb group "maint": kicad_model_check, kicad_pcm_list, kicad_pcm_install,-//! kicad_pcm_uninstall, kicad_check_for_updates, kicad_upgrade. Ported from+//! kicad_pcm_uninstall, kicad_pcm_add_repository, kicad_pcm_remove_repository,+//! kicad_check_for_updates, kicad_upgrade. Ported from //! handlers/model_check.py, handlers/pcm.py and both halves of handlers/upgrade.py (the //! installer orchestration lives in kicad_core::install_kicad). use std::time::Duration;@@ -27,25 +28,29 @@ pub static VERBS: &[Verb] = &[     },     Verb {         name: "kicad_pcm_list",-        summary: "Packages KiCad's PCM considers installed on this machine (read-only).",-        mechanism: Mechanism::File, risk: "read", timeout_sec: 30,-        input: "{}",+        summary: "Packages KiCad's PCM considers installed on this machine, each with updateAvailable from its own repository (read-only).",+        mechanism: Mechanism::File, risk: "read", timeout_sec: 60,+        input: "{\"checkUpdates\"?: true}",         example: "kicad_pcm_list {}",-        hint: "Reads installed_packages.json and checks the 3rdparty directories exist.",-        related: &["kicad_pcm_install"],-        pitfalls: &[],+        hint: "Reads installed_packages.json, checks the 3rdparty directories exist, then fetches each package's repository (matched by repository_id against kicad.json pcm.repositories) and compares the newest compatible version with the installed one: updateAvailable, latestVersion, updateCheck per package, updatesAvailable in total. checkUpdates:false skips the network.",+        related: &["kicad_pcm_install", "kicad_pcm_add_repository"],+        pitfalls: &[+            "updateCheck repository_not_registered means the package's repository is not in kicad.json (KiCad's own PCM cannot offer the update either): kicad_pcm_add_repository fixes both",+            "a pinned package reports updateAvailable false with updateCheck pinned, as PCM does",+        ],     },     Verb {         name: "kicad_pcm_install",-        summary: "Install a package from a KiCad PCM repository on disk, exactly as the Plugin and Content Manager would (no dialog).",+        summary: "Install or update a package from a KiCad PCM repository on disk, exactly as the Plugin and Content Manager would (no dialog).",         mechanism: Mechanism::File, risk: "write", timeout_sec: 900,-        input: "{\"repositoryUrl\": \"https://.../repository.json\", \"identifier\": \"inc.adom.kicad.basic-parts\", \"version\"?: \"1.0.0\", \"libPrefix\"?: \"PCM_\", \"force\"?: bool}",-        example: "kicad_pcm_install {\"repositoryUrl\":\"https://wiki.adom.inc/adom/kicad-pcm/repository.json\",\"identifier\":\"inc.adom.kicad.basic-parts\"}",-        hint: "repositoryUrl (a repository.json) + identifier; optional version. Extracts to $KICAD_3RD_PARTY/<folder>/<pkgid>/ and records installed_packages.json. KiCad registers PCM_<lib> rows itself on next load, so a running KiCad must restart (reported, never done).",-        related: &["kicad_pcm_list", "kicad_pcm_uninstall", "kicad_install_library_bundle"],+        input: "{\"repositoryUrl\"?: \"https://.../repository.json\", \"identifier\": \"com.adom.kicad-bridge.sample-lib\", \"version\"?: \"1.1.0\", \"update\"?: bool, \"libPrefix\"?: \"PCM_\", \"force\"?: bool}",+        example: "kicad_pcm_install {\"repositoryUrl\":\"https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json\",\"identifier\":\"com.adom.kicad-bridge.sample-lib\"}",+        hint: "repositoryUrl (a repository.json) + identifier installs the newest compatible version (or version). update:true is PCM's Update button: repositoryUrl may be omitted for an installed package (its own repository is used), an installed newest version answers upToDate, otherwise the old files are replaced and installed_packages.json rewritten (previousVersion, updated:true). Extracts to $KICAD_3RD_PARTY/<folder>/<pkgid>/. KiCad registers PCM_<lib> rows itself on next load, so a running KiCad must restart (reported, never done).",+        related: &["kicad_pcm_list", "kicad_pcm_uninstall", "kicad_pcm_add_repository", "kicad_install_library_bundle"],         pitfalls: &[             "a RUNNING KiCad does not see the new libraries until restarted",             "download is sha256-verified against the repository; a mismatch refuses",+            "an update removes the old package directories first; keep_on_update patterns are not honoured by the bridge",             "the native build does not detect a running KiCad yet: kicadRunning is null and restartNeeded is always true",         ],     },@@ -59,6 +64,29 @@ pub static VERBS: &[Verb] = &[         related: &["kicad_pcm_install", "kicad_pcm_list"],         pitfalls: &["restart a running KiCad afterwards"],     },+    Verb {+        name: "kicad_pcm_add_repository",+        summary: "Register a PCM repository in kicad.json (pcm.repositories), the way Plugin and Content Manager > Manage > add URL does.",+        mechanism: Mechanism::File, risk: "config", timeout_sec: 60,+        input: "{\"url\": \"https://.../repository.json\", \"name\"?: \"Adom KiCad Bridge sample PCM\"}",+        example: "kicad_pcm_add_repository {\"url\":\"https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json\"}",+        hint: "Fetches the repository.json to confirm it is a PCM repository and to take its name (name overrides, and registers it unverified when the fetch fails). Idempotent on the URL. Writes kicad.json with a .adom-bak beside it and an atomic replace; when kicad.json had no pcm.repositories key, KiCad's built-in official repository row is kept in front. KiCad reads the list at start: restartNeeded is always true.",+        related: &["kicad_pcm_remove_repository", "kicad_pcm_install", "kicad_pcm_list"],+        pitfalls: &[+            "a KiCad that is running NOW overwrites kicad.json when it saves settings on exit: close it first when in doubt",+            "the native build cannot tell whether KiCad is running: kicadRunning is null",+        ],+    },+    Verb {+        name: "kicad_pcm_remove_repository",+        summary: "Drop a PCM repository from kicad.json pcm.repositories. Installed packages stay installed.",+        mechanism: Mechanism::File, risk: "config", timeout_sec: 30,+        input: "{\"url\": \"https://.../repository.json\"}",+        example: "kicad_pcm_remove_repository {\"url\":\"https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json\"}",+        hint: "Same backup and atomic write as add; removed:false when the URL was not registered. Packages from that repository stay installed and lose their update checks.",+        related: &["kicad_pcm_add_repository", "kicad_pcm_list"],+        pitfalls: &["KiCad reads the list at start; a running KiCad keeps the old list until relaunched"],+    },     Verb {         name: "kicad_check_for_updates",         summary: "Read-only: installed KiCad vs latest stable.",@@ -93,12 +121,14 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value>             let ctx = Ctx::from_info(&state.kicad_info());             model_check::handle(&ctx, args)         }-        "kicad_pcm_list" => pcm::list(&Ctx::from_info(&state.kicad_info())),+        "kicad_pcm_list" => pcm::list(&Ctx::from_info(&state.kicad_info()), args, &pcm::http_fetch),         "kicad_pcm_install" => {             let ctx = Ctx::from_info(&state.kicad_info());             pcm::install(&ctx, args, &pcm::http_fetch)         }         "kicad_pcm_uninstall" => pcm::uninstall(&Ctx::from_info(&state.kicad_info()), args),+        "kicad_pcm_add_repository" => pcm::add_repository(&Ctx::from_info(&state.kicad_info()), args, &pcm::http_fetch),+        "kicad_pcm_remove_repository" => pcm::remove_repository(&Ctx::from_info(&state.kicad_info()), args),         "kicad_check_for_updates" => {             let info = state.kicad_info();             let current = info.primary().map(|i| if i.cli_version.is_empty() { i.version.clone() } else { i.cli_version.clone() });
rust/crates/kicad-core/src/pcm.rs+646−38
@@ -23,9 +23,19 @@ //! own PCM dialog shows a bridge-installed package as Installed, offers updates for it, //! and can uninstall it. The bridge is an automation of PCM, not a parallel mechanism. //!+//! Repositories are the other half of PCM's on-disk contract: kicad.json+//! `pcm.repositories` is `[{"name": ..., "url": ...}]` (kicad_settings.cpp), read at+//! KiCad start. `add_repository` / `remove_repository` edit it the way the Manage+//! dialog does, and `list` uses it to answer "is there an update" per installed+//! package by fetching each package's own repository (matched by repository_id) and+//! comparing versions the way PCM orders them. `install {update:true}` is the headless+//! Update button: it replaces the installed version and its registry row.+//! //! Neither `ring` nor `flate2` is a declared dependency of this crate, so the SHA-256 //! and the zip reader (stored and deflate entries, no zip64) live here on std alone. +use std::cmp::Ordering;+use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -37,6 +47,11 @@ pub const FOLDERS: &[&str] = &["plugins", "footprints", "symbols", "3dmodels", " pub const MAX_ZIP: u64 = 512 * 1024 * 1024; const UA: &str = "adom-kicad-bridge pcm"; +/// KiCad's built-in repository list (kicad_settings.cpp PCM_DEFAULT_REPOSITORY_NAME/URL):+/// what `pcm.repositories` means when kicad.json does not carry the key at all.+pub const DEFAULT_REPOSITORY_NAME: &str = "KiCad official repository";+pub const DEFAULT_REPOSITORY_URL: &str = "https://repository.kicad.org/repository.json";+ /// Bytes fetcher: (url, timeout) -> bytes. Production uses `http_fetch`; tests hand in a table. pub type Fetch<'a> = &'a dyn Fn(&str, Duration) -> Result<Vec<u8>, String>; @@ -433,6 +448,297 @@ impl<'a> ZipFile<'a> {     } } +// ── Versions, the way PCM orders them ─────────────────────────────────────────++/// Dotted version order: each component numerically ("1.10.0" > "1.9.0"), missing+/// components are 0 ("1.0" == "1.0.0"), a non-numeric component counts as 0 and the+/// raw strings break the tie so odd versions still order deterministically.+pub fn compare_versions(a: &str, b: &str) -> Ordering {+    let parse = |s: &str| -> (Vec<u64>, bool) {+        let mut lossy = false;+        let parts = s.trim().split('.').map(|p| p.trim().parse::<u64>().unwrap_or_else(|_| { lossy = true; 0 })).collect();+        (parts, lossy)+    };+    let ((pa, la), (pb, lb)) = (parse(a), parse(b));+    let n = pa.len().max(pb.len());+    for i in 0..n {+        let (x, y) = (pa.get(i).copied().unwrap_or(0), pb.get(i).copied().unwrap_or(0));+        match x.cmp(&y) {+            Ordering::Equal => continue,+            o => return o,+        }+    }+    if la || lb {+        a.trim().cmp(b.trim())+    } else {+        Ordering::Equal+    }+}++/// Order two PackageVersion objects: `version_epoch` first (a higher epoch is always+/// later, as the schema says), then `compare_versions` on `version`.+pub fn version_cmp(a: &Value, b: &Value) -> Ordering {+    let epoch = |v: &Value| v.get("version_epoch").and_then(Value::as_u64).unwrap_or(0);+    let ver = |v: &Value| v.get("version").and_then(Value::as_str).unwrap_or("").to_string();+    epoch(a).cmp(&epoch(b)).then_with(|| compare_versions(&ver(a), &ver(b)))+}++/// PCM's compatibility filter: `kicad_version` (minimum) <= this KiCad and, when present,+/// `kicad_version_max` >= this KiCad, compared on major.minor. An unknown KiCad version+/// (no detected install) accepts everything.+pub fn version_compatible(v: &Value, kicad_version: &str) -> bool {+    let mm = |s: &str| s.split('.').take(2).collect::<Vec<_>>().join(".");+    if kicad_version.trim().is_empty() {+        return true;+    }+    let here = mm(kicad_version);+    if let Some(min) = v.get("kicad_version").and_then(Value::as_str).filter(|s| !s.is_empty()) {+        if compare_versions(&mm(min), &here) == Ordering::Greater {+            return false;+        }+    }+    if let Some(max) = v.get("kicad_version_max").and_then(Value::as_str).filter(|s| !s.is_empty()) {+        if compare_versions(&mm(max), &here) == Ordering::Less {+            return false;+        }+    }+    true+}++/// The version PCM would offer: the highest compatible one that has a download_url.+pub fn latest_version<'a>(versions: &'a [Value], kicad_version: &str) -> Option<&'a Value> {+    versions+        .iter()+        .filter(|v| v.get("download_url").and_then(Value::as_str).map(|u| !u.is_empty()).unwrap_or(false))+        .filter(|v| version_compatible(v, kicad_version))+        .max_by(|a, b| version_cmp(a, b))+}++// ── Repositories (kicad.json pcm.repositories) ────────────────────────────────++/// PCM's repository_id: sha256(url) truncated to 16 hex chars (pcm.cpp SetRepositoryList).+pub fn repository_id(url: &str) -> String {+    sha256_hex(url.as_bytes())[..16].to_string()+}++fn kicad_json_path(ctx: &Ctx) -> Option<PathBuf> {+    ctx.config_dir.as_ref().map(|c| c.join("kicad.json"))+}++fn read_kicad_json(path: &Path) -> Result<Value, String> {+    match std::fs::read_to_string(path) {+        Ok(text) => {+            let v: Value = serde_json::from_str(&text).map_err(|e| format!("{}: {e}", norm(path)))?;+            if v.is_object() {+                Ok(v)+            } else {+                Err(format!("{}: top level is not a JSON object", norm(path)))+            }+        }+        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(json!({"meta": {"filename": "kicad", "version": 0}})),+        Err(e) => Err(format!("{}: {e}", norm(path))),+    }+}++/// The `(name, url)` rows KiCad's PCM will use: `pcm.repositories` from kicad.json, or+/// KiCad's built-in default when the key is absent (which is how a fresh KiCad looks).+pub fn registered_repositories(ctx: &Ctx) -> Vec<(String, String)> {+    let doc = kicad_json_path(ctx).and_then(|p| read_kicad_json(&p).ok()).unwrap_or(Value::Null);+    repositories_of(&doc)+}++fn repositories_of(doc: &Value) -> Vec<(String, String)> {+    match doc.get("pcm").and_then(|p| p.get("repositories")).and_then(Value::as_array) {+        Some(rows) => rows+            .iter()+            .filter_map(|r| {+                let url = r.get("url").and_then(Value::as_str)?.to_string();+                let name = r.get("name").and_then(Value::as_str).unwrap_or("").to_string();+                Some((name, url))+            })+            .collect(),+        None => vec![(DEFAULT_REPOSITORY_NAME.to_string(), DEFAULT_REPOSITORY_URL.to_string())],+    }+}++fn repositories_json(rows: &[(String, String)]) -> Value {+    Value::Array(rows.iter().map(|(n, u)| json!({"name": n, "url": u})).collect())+}++fn repositories_report(rows: &[(String, String)]) -> Value {+    Value::Array(rows.iter().map(|(n, u)| json!({"name": n, "url": u, "repositoryId": repository_id(u)})).collect())+}++/// `.adom-bak` beside the file (when it exists), then tmp + rename. Two-space indent,+/// which is how KiCad's own settings writer (nlohmann dump(2)) lays kicad.json out.+fn write_kicad_json(path: &Path, doc: &Value) -> Result<Option<PathBuf>, String> {+    if let Some(parent) = path.parent() {+        std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", norm(parent)))?;+    }+    let mut backup = None;+    if path.is_file() {+        let b = path.with_file_name(format!("{}.adom-bak", path.file_name().unwrap_or_default().to_string_lossy()));+        std::fs::copy(path, &b).map_err(|e| format!("cannot back up {}: {e}", norm(path)))?;+        backup = Some(b);+    }+    let mut buf = Vec::new();+    let mut ser = serde_json::Serializer::with_formatter(&mut buf, serde_json::ser::PrettyFormatter::with_indent(b"  "));+    serde::Serialize::serialize(doc, &mut ser).map_err(|e| e.to_string())?;+    buf.push(b'\n');+    let tmp = path.with_file_name(format!("{}.adom-tmp", path.file_name().unwrap_or_default().to_string_lossy()));+    std::fs::write(&tmp, &buf).and_then(|_| std::fs::rename(&tmp, path)).map_err(|e| {+        let _ = std::fs::remove_file(&tmp);+        format!("cannot write {}: {e}", norm(path))+    })?;+    Ok(backup)+}++const REPO_RESTART_NOTE: &str = "KiCad reads pcm.repositories at start: a running KiCad keeps its old list until it is closed and launched again, and a KiCad that is running NOW will overwrite this edit when it saves its settings on exit, so close KiCad first when in doubt (the native build cannot tell whether KiCad is running: kicadRunning is null).";++fn valid_repo_url(u: &str) -> bool {+    (u.starts_with("https://") || u.starts_with("http://") || u.starts_with("file://")) && !u[u.find("://").unwrap_or(0) + 3..].is_empty()+}++/// kicad_pcm_add_repository {url, name?}: register a PCM repository in kicad.json the way+/// the Manage dialog does. The repository.json is fetched to confirm it is one and to+/// take its name when none is given. Idempotent on the URL.+pub fn add_repository(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {+    let s = |k: &str| args.get(k).and_then(Value::as_str).map(|v| v.trim().to_string()).filter(|v| !v.is_empty());+    let Some(url) = s("url") else {+        return json!({"success": false, "errorCode": "invalid_arg", "error": "url is required",+            "_hint": "url is the repository.json URL, e.g. https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm/repository.json"});+    };+    if !valid_repo_url(&url) {+        return json!({"success": false, "errorCode": "invalid_arg", "error": format!("not a repository URL: {url}"),+            "_hint": "PCM accepts http(s):// and file:// URLs that point at a repository.json."});+    }+    let Some(path) = kicad_json_path(ctx) else {+        return json!({"success": false, "errorCode": "settings_not_found", "error": "KiCad config directory unknown",+            "_hint": "kicad_readiness first; the bridge needs a detected KiCad to know where kicad.json lives."});+    };+    // Confirm it is a PCM repository (and learn its name), like the dialog does.+    let (fetched, fetched_name, fetch_error) = match fetch(&url, Duration::from_secs(30)) {+        Ok(bytes) => match serde_json::from_slice::<Value>(&bytes) {+            Ok(v) if v.get("packages").and_then(|p| p.get("url")).is_some() => (true, v.get("name").and_then(Value::as_str).map(String::from), None),+            Ok(_) => (false, None, Some("the URL serves JSON, but not a PCM repository (no packages.url)".to_string())),+            Err(e) => (false, None, Some(format!("the URL does not serve JSON: {e}"))),+        },+        Err(e) => (false, None, Some(e)),+    };+    let name = match s("name").or(fetched_name) {+        Some(n) => n,+        None => {+            return json!({"success": false, "errorCode": "repository_unreachable", "url": url,+                "error": format!("could not read the repository and no name was given: {}", fetch_error.clone().unwrap_or_default()),+                "_hint": "Check the URL serves a repository.json anonymously (curl it), or pass name to register it unverified."});+        }+    };+    let mut doc = match read_kicad_json(&path) {+        Ok(d) => d,+        Err(e) => return json!({"success": false, "errorCode": "settings_unreadable", "error": e}),+    };+    let mut rows = repositories_of(&doc);+    let existing = rows.iter().find(|(_, u)| u == &url).cloned();+    let mut out = json!({+        "success": true, "url": url, "repositoryId": repository_id(&url),+        "file": norm(&path), "verified": fetched,+        "kicadRunning": Value::Null, "restartNeeded": true,+    });+    if let Some(e) = fetch_error {+        out["warning"] = json!(format!("registered unverified: {e}"));+    }+    if let Some((n, _)) = existing {+        out["name"] = json!(n);+        out["added"] = json!(false);+        out["alreadyRegistered"] = json!(true);+        out["repositories"] = repositories_report(&rows);+        out["_hint"] = json!(format!("Already in pcm.repositories as '{n}'; nothing written. {REPO_RESTART_NOTE}"));+        return out;+    }+    rows.push((name.clone(), url.clone()));+    let obj = doc.as_object_mut().unwrap();+    if !obj.get("pcm").map(Value::is_object).unwrap_or(false) {+        obj.insert("pcm".into(), Value::Object(Map::new()));+    }+    obj["pcm"]["repositories"] = repositories_json(&rows);+    match write_kicad_json(&path, &doc) {+        Ok(backup) => {+            out["name"] = json!(name);+            out["added"] = json!(true);+            out["alreadyRegistered"] = json!(false);+            out["backup"] = backup.map(|b| json!(norm(&b))).unwrap_or(Value::Null);+            out["repositories"] = repositories_report(&rows);+            out["_hint"] = json!(format!("Written to kicad.json pcm.repositories (KiCad's own default list was kept in front of it). {REPO_RESTART_NOTE} Then kicad_pcm_install {{\"repositoryUrl\": \"{url}\", \"identifier\": ...}} installs from it without the dialog."));+            out+        }+        Err(e) => json!({"success": false, "errorCode": "settings_write_failed", "error": e,+            "_hint": "The original kicad.json is intact (atomic replace). Check the directory is writable."}),+    }+}++/// kicad_pcm_remove_repository {url}: drop a row from pcm.repositories. Packages+/// installed from it stay installed (PCM keeps them too; they just lose update checks).+pub fn remove_repository(ctx: &Ctx, args: &Value) -> Value {+    let Some(url) = args.get("url").and_then(Value::as_str).map(str::trim).filter(|s| !s.is_empty()) else {+        return json!({"success": false, "errorCode": "invalid_arg", "error": "url is required"});+    };+    let Some(path) = kicad_json_path(ctx) else {+        return json!({"success": false, "errorCode": "settings_not_found", "error": "KiCad config directory unknown"});+    };+    let mut doc = match read_kicad_json(&path) {+        Ok(d) => d,+        Err(e) => return json!({"success": false, "errorCode": "settings_unreadable", "error": e}),+    };+    let mut rows = repositories_of(&doc);+    let before = rows.len();+    rows.retain(|(_, u)| u != url);+    if rows.len() == before {+        return json!({"success": true, "url": url, "removed": false, "file": norm(&path), "repositories": repositories_report(&rows),+            "kicadRunning": Value::Null, "restartNeeded": false,+            "_hint": "That URL is not in pcm.repositories; nothing written."});+    }+    let obj = doc.as_object_mut().unwrap();+    if !obj.get("pcm").map(Value::is_object).unwrap_or(false) {+        obj.insert("pcm".into(), Value::Object(Map::new()));+    }+    obj["pcm"]["repositories"] = repositories_json(&rows);+    match write_kicad_json(&path, &doc) {+        Ok(backup) => json!({"success": true, "url": url, "removed": true, "file": norm(&path),+            "backup": backup.map(|b| json!(norm(&b))).unwrap_or(Value::Null),+            "repositories": repositories_report(&rows), "kicadRunning": Value::Null, "restartNeeded": true,+            "_hint": format!("Removed from kicad.json pcm.repositories. Packages installed from it stay installed. {REPO_RESTART_NOTE}")}),+        Err(e) => json!({"success": false, "errorCode": "settings_write_failed", "error": e}),+    }+}++// ── Fetching a repository ─────────────────────────────────────────────────────++pub enum RepoError {+    /// packages.json bytes do not hash to repository.json's packages.sha256.+    Hash,+    Other(String),+}++/// repository.json -> packages.json, sha256-checked the way PCM checks it.+/// Returns (repository document, package array).+pub fn fetch_repository(fetch: Fetch, repo_url: &str, timeout: Duration) -> Result<(Value, Vec<Value>), RepoError> {+    let repo: Value = serde_json::from_slice(&fetch(repo_url, timeout).map_err(RepoError::Other)?).map_err(|e| RepoError::Other(format!("JSONDecodeError: {e}")))?;+    let pkgs_url = repo+        .get("packages")+        .and_then(|p| p.get("url"))+        .and_then(Value::as_str)+        .ok_or_else(|| RepoError::Other("KeyError: 'packages'".into()))?+        .to_string();+    let raw = fetch(&pkgs_url, timeout).map_err(RepoError::Other)?;+    let want_sha = repo.get("packages").and_then(|p| p.get("sha256")).and_then(Value::as_str).unwrap_or("");+    if sha256_hex(&raw) != want_sha {+        return Err(RepoError::Hash);+    }+    let pk: Value = serde_json::from_slice(&raw).map_err(|e| RepoError::Other(format!("JSONDecodeError: {e}")))?;+    let list = pk.get("packages").and_then(Value::as_array).cloned().ok_or_else(|| RepoError::Other("KeyError: 'packages'".into()))?;+    Ok((repo, list))+}+ // ── PCM on disk ───────────────────────────────────────────────────────────────  pub struct Paths {@@ -566,39 +872,71 @@ fn running_block() -> (Value, bool) {     (Value::Null, true) } -/// kicad_pcm_install {repositoryUrl, identifier, version?, libPrefix?, force?}.+/// The repository URL an installed package came from: its registry row's repository_id+/// matched against the registered repositories (by id, then by name as a fallback).+fn repository_url_for(installed: &Value, ident: &str, repos: &[(String, String)]) -> Option<String> {+    let entry = installed["packages"].as_array()?.iter().find(|e| ident_of(e) == Some(ident))?;+    let id = entry.get("repository_id").and_then(Value::as_str).unwrap_or("");+    let name = entry.get("repository_name").and_then(Value::as_str).unwrap_or("");+    repos+        .iter()+        .find(|(_, u)| !id.is_empty() && repository_id(u) == id)+        .or_else(|| repos.iter().find(|(n, _)| !name.is_empty() && n == name))+        .map(|(_, u)| u.clone())+}++/// kicad_pcm_install {repositoryUrl?, identifier, version?, update?, libPrefix?, force?}.+///+/// `update:true` is PCM's Update button without the dialog: the repository is taken+/// from the installed package's own registry row when repositoryUrl is omitted, the+/// newest compatible version is chosen, an installed version that is already the newest+/// answers upToDate, and otherwise the old files are replaced and installed_packages.json+/// rewritten (previousVersion says what was there). A plain install of a version that+/// differs from the installed one is the same replacement; `force` reinstalls the same one. pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {     let s = |k: &str| args.get(k).and_then(Value::as_str).map(|v| v.trim().to_string()).filter(|v| !v.is_empty());     let (auto_add, default_prefix) = pcm_prefs(ctx);     let prefix = s("libPrefix").unwrap_or(default_prefix);-    let (Some(repo_url), Some(ident)) = (s("repositoryUrl"), s("identifier")) else {-        return json!({"success": false, "error": "repositoryUrl and identifier are required",-            "_hint": "repositoryUrl is the repository.json URL; identifier like inc.adom.kicad.basic-parts."});+    let update = args.get("update").and_then(Value::as_bool).unwrap_or(false);+    let Some(ident) = s("identifier") else {+        return json!({"success": false, "error": "identifier is required",+            "_hint": "identifier like com.adom.kicad-bridge.sample-lib; repositoryUrl is the repository.json URL (optional for an installed package: its own repository is used)."});     };     let want = s("version");     let Some(paths) = paths(ctx) else {         return json!({"success": false, "error": "KiCad user/config directories unknown",             "_hint": "kicad_readiness first; the bridge needs a detected KiCad to know 3rdparty and settings paths."});     };-    let repo_result: Result<(Value, Vec<Value>), String> = (|| {-        let repo: Value = serde_json::from_slice(&fetch(&repo_url, Duration::from_secs(120))?).map_err(|e| format!("JSONDecodeError: {e}"))?;-        let pkgs_url = repo.get("packages").and_then(|p| p.get("url")).and_then(Value::as_str).ok_or("KeyError: 'packages'")?.to_string();-        let raw = fetch(&pkgs_url, Duration::from_secs(120))?;-        let want_sha = repo.get("packages").and_then(|p| p.get("sha256")).and_then(Value::as_str).unwrap_or("");-        if sha256_hex(&raw) != want_sha {-            return Err("__hash__".into());-        }-        let pk: Value = serde_json::from_slice(&raw).map_err(|e| format!("JSONDecodeError: {e}"))?;-        let list = pk.get("packages").and_then(Value::as_array).cloned().ok_or("KeyError: 'packages'")?;-        Ok((repo, list))-    })();-    let (repo, packages) = match repo_result {+    let mut installed = read_installed(&paths.installed);+    let previous: Option<String> = installed["packages"]+        .as_array()+        .and_then(|a| a.iter().find(|e| ident_of(e) == Some(ident.as_str())))+        .and_then(|e| e.get("current_version").and_then(Value::as_str))+        .map(String::from);+    let repo_url = match s("repositoryUrl") {+        Some(u) => u,+        None => {+            if previous.is_none() {+                return json!({"success": false, "errorCode": "not_installed", "error": format!("repositoryUrl is required: {ident} is not installed, so there is no repository to take it from"),+                    "_hint": "repositoryUrl is required for a first install; update:true on an installed package finds its repository itself."});+            }+            match repository_url_for(&installed, &ident, &registered_repositories(ctx)) {+                Some(u) => u,+                None => {+                    return json!({"success": false, "errorCode": "repository_not_registered",+                        "error": format!("{ident} is installed but its repository is not in kicad.json pcm.repositories"),+                        "_hint": "Pass repositoryUrl, or kicad_pcm_add_repository {url} first so KiCad's PCM can see it too."});+                }+            }+        }+    };+    let (repo, packages) = match fetch_repository(fetch, &repo_url, Duration::from_secs(120)) {         Ok(v) => v,-        Err(e) if e == "__hash__" => {+        Err(RepoError::Hash) => {             return json!({"success": false, "error": "packages.json hash does not match repository.json",                 "_hint": "The repository is mid-update or corrupted; retry in a minute."})         }-        Err(e) => return json!({"success": false, "error": format!("could not read repository: {e}")}),+        Err(RepoError::Other(e)) => return json!({"success": false, "error": format!("could not read repository: {e}")}),     };     let Some(pkg) = packages.iter().find(|p| p.get("identifier").and_then(Value::as_str) == Some(ident.as_str())) else {         let available: Vec<Value> = packages.iter().take(20).map(|p| p.get("identifier").cloned().unwrap_or(Value::Null)).collect();@@ -613,9 +951,10 @@ pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {                 return json!({"success": false, "error": format!("version {w} not offered"), "available": available});             }         },-        None => match versions.last() {+        None => match latest_version(&versions, &ctx.version) {             Some(v) => v.clone(),-            None => return json!({"success": false, "error": "package has no downloadable version"}),+            None => return json!({"success": false, "error": "package has no downloadable version",+                "_hint": format!("No version with a download_url is compatible with KiCad {}.", ctx.version)}),         },     };     let Some(download_url) = vsel.get("download_url").and_then(Value::as_str).filter(|u| !u.is_empty()).map(String::from) else {@@ -623,12 +962,17 @@ pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {     };     let version = vsel.get("version").and_then(Value::as_str).unwrap_or("").to_string(); -    let mut installed = read_installed(&paths.installed);     let force = args.get("force").and_then(Value::as_bool).unwrap_or(false);-    let prior_same = installed["packages"]-        .as_array()-        .map(|a| a.iter().any(|e| ident_of(e) == Some(ident.as_str()) && e.get("current_version").and_then(Value::as_str) == Some(version.as_str())))-        .unwrap_or(false);+    let prior_same = previous.as_deref() == Some(version.as_str());+    if update && want.is_none() {+        if let Some(prev) = &previous {+            if compare_versions(&version, prev) != Ordering::Greater && !force {+                return json!({"success": true, "upToDate": true, "identifier": ident, "version": prev, "latestVersion": version,+                    "repositoryUrl": repo_url,+                    "_hint": "Installed version is the newest the repository offers; nothing changed. Pass version to install a specific one, or force:true to reinstall."});+            }+        }+    }     if prior_same && !force {         return json!({"success": true, "alreadyInstalled": true, "identifier": ident, "version": version,             "_hint": "Already installed at this version; pass force:true to reinstall."});@@ -663,7 +1007,7 @@ pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {     let entry = json!({         "package": package,         "current_version": version,-        "repository_id": &sha256_hex(repo_url.as_bytes())[..16],+        "repository_id": repository_id(&repo_url),         "repository_name": repo.get("name").and_then(Value::as_str).unwrap_or(""),         "install_timestamp": now_secs(),         "pinned": false,@@ -678,13 +1022,20 @@ pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {      let (running, restart) = running_block();     let nick = |names: &[String]| -> Vec<String> { names.iter().map(|n| format!("{prefix}{n}")).collect() };-    let mut hint = String::from(-        "Installed exactly as PCM would; KiCad's PCM dialog will list it as Installed. Library nicknames above are created by KiCad's own PCM scan when the global library table next LOADS, so a KiCad that is already running must be restarted (never done for you) before the libraries appear.");+    let replaced = previous.as_ref().map(|p| p != &version).unwrap_or(false);+    let mut hint = if replaced {+        format!("Replaced {} with {version} exactly as PCM's Update does (old files removed, registry row rewritten; keep_on_update patterns are not honoured by the bridge). ", previous.clone().unwrap_or_default())+    } else {+        String::from("Installed exactly as PCM would; KiCad's PCM dialog will list it as Installed. ")+    };+    hint.push_str("Library nicknames above are created by KiCad's own PCM scan when the global library table next LOADS, so a KiCad that is already running must be restarted (never done for you) before the libraries appear.");     if !auto_add {         hint.push_str(" kicad.json pcm.lib_auto_add is OFF on this box, so KiCad will NOT add the rows itself: register the libraries with kicad_install_library or turn lib_auto_add back on.");     }     json!({         "success": true, "identifier": ident, "version": version,+        "previousVersion": previous, "updated": replaced,+        "repositoryUrl": repo_url,         "filesInstalled": ext.files,         "installedTo": norm(&paths.third.join("*").join(&ext.package_dir)),         "thirdPartyDir": norm(&paths.third), "thirdPartyResolvedBy": paths.resolved_by,@@ -697,12 +1048,20 @@ pub fn install(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {     }) } -/// kicad_pcm_list {}: packages KiCad's PCM considers installed (read-only).-pub fn list(ctx: &Ctx) -> Value {+/// kicad_pcm_list {checkUpdates?: true}: packages KiCad's PCM considers installed, each+/// with `updateAvailable` from its own repository (the registry row's repository_id+/// matched against kicad.json pcm.repositories, then repository.json -> packages.json,+/// sha256-checked, newest compatible version compared with current_version).+/// checkUpdates:false skips the network and leaves the update fields null.+pub fn list(ctx: &Ctx, args: &Value, fetch: Fetch) -> Value {     let Some(paths) = paths(ctx) else {         return json!({"success": false, "error": "KiCad config directory unknown"});     };+    let check = args.get("checkUpdates").and_then(Value::as_bool).unwrap_or(true);+    let repos = registered_repositories(ctx);     let d = read_installed(&paths.installed);+    let mut cache: HashMap<String, Result<Vec<Value>, String>> = HashMap::new();+    let mut updates = 0usize;     let out: Vec<Value> = d["packages"]         .as_array()         .into_iter()@@ -710,13 +1069,60 @@ pub fn list(ctx: &Ctx) -> Value {         .map(|e| {             let p = e.get("package").cloned().unwrap_or(Value::Null);             let id = p.get("identifier").and_then(Value::as_str).unwrap_or("");+            let current = e.get("current_version").and_then(Value::as_str).unwrap_or("");             let on_disk = FOLDERS.iter().any(|f| paths.third.join(f).join(pkgdir(id)).exists());-            json!({"identifier": p.get("identifier"), "name": p.get("name"), "type": p.get("type"),-                   "version": e.get("current_version"), "repository": e.get("repository_name"), "onDisk": on_disk})+            let mut row = json!({"identifier": p.get("identifier"), "name": p.get("name"), "type": p.get("type"),+                   "version": e.get("current_version"), "repository": e.get("repository_name"),+                   "repositoryId": e.get("repository_id"), "pinned": e.get("pinned"), "onDisk": on_disk,+                   "updateAvailable": Value::Null, "latestVersion": Value::Null, "repositoryUrl": Value::Null,+                   "updateCheck": if check { "repository_not_registered" } else { "skipped" }});+            if !check {+                return row;+            }+            let Some(url) = repository_url_for(&d, id, &repos) else { return row };+            row["repositoryUrl"] = json!(url);+            let fetched = cache.entry(url.clone()).or_insert_with(|| match fetch_repository(fetch, &url, Duration::from_secs(20)) {+                Ok((_, list)) => Ok(list),+                Err(RepoError::Hash) => Err("packages.json hash does not match repository.json".into()),+                Err(RepoError::Other(m)) => Err(m),+            });+            match fetched {+                Err(m) => row["updateCheck"] = json!(format!("fetch_failed: {m}")),+                Ok(list) => match list.iter().find(|q| q.get("identifier").and_then(Value::as_str) == Some(id)) {+                    None => row["updateCheck"] = json!("package_not_in_repository"),+                    Some(q) => {+                        let versions = q.get("versions").and_then(Value::as_array).cloned().unwrap_or_default();+                        match latest_version(&versions, &ctx.version) {+                            None => row["updateCheck"] = json!("no_compatible_version"),+                            Some(v) => {+                                let latest = v.get("version").and_then(Value::as_str).unwrap_or("");+                                let pinned = e.get("pinned").and_then(Value::as_bool).unwrap_or(false);+                                let newer = compare_versions(latest, current) == Ordering::Greater;+                                if newer && !pinned {+                                    updates += 1;+                                }+                                row["latestVersion"] = json!(latest);+                                row["updateAvailable"] = json!(newer && !pinned);+                                row["updateCheck"] = json!(if newer && pinned { "pinned" } else { "ok" });+                            }+                        }+                    }+                },+            }+            row         })         .collect();-    json!({"success": true, "count": out.len(), "packages": out,-           "thirdPartyDir": norm(&paths.third), "thirdPartyResolvedBy": paths.resolved_by, "kicadVersion": paths.version})+    let hint = if !check {+        "checkUpdates:false: no repository was fetched.".to_string()+    } else if updates > 0 {+        format!("{updates} package(s) have a newer version in their repository: kicad_pcm_install {{\"identifier\": ..., \"update\": true}} applies it (KiCad's own PCM dialog shows the same Update button after a restart).")+    } else {+        "Every installed package is at the newest version its repository offers (updateCheck says why when a check could not run).".to_string()+    };+    json!({"success": true, "count": out.len(), "packages": out, "updatesAvailable": updates,+           "repositories": repositories_report(&repos),+           "thirdPartyDir": norm(&paths.third), "thirdPartyResolvedBy": paths.resolved_by, "kicadVersion": paths.version,+           "_hint": hint}) }  /// kicad_pcm_uninstall {identifier}: remove the 3rdparty directories and the record.@@ -811,6 +1217,9 @@ mod tests {     fn ctx(root: &Path) -> Ctx {         Ctx { version: "10.0.6".into(), base_dir: None, config_dir: Some(root.join("config")), user_dir: Some(root.join("Documents/KiCad/10.0")), common: Value::Null }     }+    fn read_json(p: &Path) -> Value {+        serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()+    }     const REPO_URL: &str = "https://wiki.adom.inc/adom/kicad-pcm/repository.json";     fn repo_table(download_sha: Option<&str>) -> HashMap<String, Vec<u8>> {         let mut v = json!({"version": "1.0.0", "download_url": "https://example.test/pkg.zip"});@@ -876,8 +1285,10 @@ mod tests {         let r = install(&c, &json!({"repositoryUrl": REPO_URL, "identifier": "inc.adom.kicad.test", "force": true, "libPrefix": "X_"}), &f);         assert_eq!(r["symbolLibraries"], json!(["X_Adom"]));         // list-        let l = list(&c);+        let l = list(&c, &json!({"checkUpdates": false}), &f);         assert_eq!(l["count"], json!(1));+        assert_eq!(l["packages"][0]["updateCheck"], json!("skipped"));+        assert!(l["packages"][0]["updateAvailable"].is_null());         assert_eq!(l["packages"][0]["identifier"], json!("inc.adom.kicad.test"));         assert_eq!(l["packages"][0]["onDisk"], json!(true));         assert_eq!(l["kicadVersion"], json!("10.0.6"));@@ -886,7 +1297,7 @@ mod tests {         assert_eq!(u["recordRemoved"], json!(true));         assert_eq!(u["removedDirs"].as_array().unwrap().len(), 5);         assert!(!t.0.join("Documents/KiCad/10.0/3rdparty/symbols/inc_adom_kicad_test").exists());-        assert_eq!(list(&c)["count"], json!(0));+        assert_eq!(list(&c, &json!({}), &f)["count"], json!(0));         let u = uninstall(&c, &json!({"identifier": "inc.adom.kicad.test"}));         assert_eq!(u["recordRemoved"], json!(false));     }@@ -920,7 +1331,7 @@ mod tests {         // no KiCad dirs known         let r = install(&Ctx::default(), &json!({"repositoryUrl": REPO_URL, "identifier": "x"}), &f);         assert!(r["error"].as_str().unwrap().contains("directories unknown"));-        assert_eq!(list(&Ctx::default())["success"], json!(false));+        assert_eq!(list(&Ctx::default(), &json!({}), &f)["success"], json!(false));     }      #[test]@@ -954,4 +1365,201 @@ mod tests {         assert!(text.ends_with("}\n"));         assert_eq!(read_installed(&p)["packages"][0]["current_version"], json!("1"));     }++    #[test]+    fn version_order_matches_pcm() {+        use std::cmp::Ordering::*;+        assert_eq!(compare_versions("1.0.0", "1.0"), Equal);+        assert_eq!(compare_versions("1.1.0", "1.0.0"), Greater);+        assert_eq!(compare_versions("1.10.0", "1.9.0"), Greater, "numeric, not lexical");+        assert_eq!(compare_versions("2", "1.99.99"), Greater);+        assert_eq!(compare_versions("0.9", "1.0.0"), Less);+        assert_eq!(compare_versions("1.0.0-rc1", "1.0.0"), Greater, "non-numeric tail ties on numbers, then the string decides");+        // epoch wins over the dotted number+        let a = json!({"version": "1.0.0", "version_epoch": 1});+        let b = json!({"version": "9.9.9"});+        assert_eq!(version_cmp(&a, &b), Greater);+        assert_eq!(version_cmp(&json!({"version": "1.2"}), &json!({"version": "1.10"})), Less);+        // compatibility on major.minor+        let v = json!({"version": "1", "kicad_version": "10.0", "download_url": "u"});+        assert!(version_compatible(&v, "10.0.5"));+        assert!(!version_compatible(&v, "9.0.4"));+        assert!(version_compatible(&v, ""), "unknown KiCad accepts everything");+        let capped = json!({"version": "1", "kicad_version": "8.0", "kicad_version_max": "9.0", "download_url": "u"});+        assert!(version_compatible(&capped, "9.0.2"));+        assert!(!version_compatible(&capped, "10.0.5"));+        // latest: highest compatible with a download_url+        let versions = vec![+            json!({"version": "1.0.0", "kicad_version": "10.0", "download_url": "a"}),+            json!({"version": "1.2.0", "kicad_version": "11.0", "download_url": "c"}),+            json!({"version": "1.1.0", "kicad_version": "10.0", "download_url": "b"}),+            json!({"version": "1.3.0", "kicad_version": "10.0"}),+        ];+        assert_eq!(latest_version(&versions, "10.0.5").unwrap()["version"], json!("1.1.0"));+        assert_eq!(latest_version(&versions, "11.0.0").unwrap()["version"], json!("1.2.0"));+        assert!(latest_version(&versions[3..], "10.0.5").is_none(), "no download_url, nothing to offer");+        assert_eq!(repository_id("https://repository.kicad.org/repository.json").len(), 16);+    }++    #[test]+    fn repositories_edit_round_trip() {+        let t = tmp("repos");+        let c = ctx(&t.0);+        let cfg = t.0.join("config");+        let f = fetcher(repo_table(Some(ZIP_SHA)));+        // Nothing registered yet: KiCad's built-in default is what PCM would use.+        assert_eq!(registered_repositories(&c), vec![(DEFAULT_REPOSITORY_NAME.to_string(), DEFAULT_REPOSITORY_URL.to_string())]);+        // Validation.+        assert_eq!(add_repository(&c, &json!({}), &f)["errorCode"], json!("invalid_arg"));+        assert_eq!(add_repository(&c, &json!({"url": "ftp://x/y.json"}), &f)["errorCode"], json!("invalid_arg"));+        let r = add_repository(&c, &json!({"url": "https://example.test/none.json"}), &f);+        assert_eq!(r["errorCode"], json!("repository_unreachable"));+        assert!(!cfg.join("kicad.json").exists(), "a refused add writes nothing");+        // Unverified add with an explicit name still lands, with a warning.+        let r = add_repository(&c, &json!({"url": "https://example.test/none.json", "name": "Blind"}), &f);+        assert_eq!(r["success"], json!(true), "{r}");+        assert_eq!(r["verified"], json!(false));+        assert!(r["warning"].as_str().unwrap().contains("unverified"));+        assert!(r["backup"].is_null(), "no kicad.json existed, so nothing to back up");+        // First add on a fresh kicad.json: the official row is kept in front.+        let r = add_repository(&c, &json!({"url": REPO_URL}), &f);+        assert_eq!(r["success"], json!(true), "{r}");+        assert_eq!(r["added"], json!(true));+        assert_eq!(r["verified"], json!(true));+        assert_eq!(r["name"], json!("Adom PCM"), "name taken from the fetched repository.json");+        assert_eq!(r["repositoryId"], json!(repository_id(REPO_URL)));+        assert_eq!(r["restartNeeded"], json!(true));+        assert!(r["kicadRunning"].is_null());+        let bak = cfg.join("kicad.json.adom-bak");+        assert!(bak.exists());+        assert_eq!(r["backup"], json!(norm(&bak)));+        let doc = read_json(&cfg.join("kicad.json"));+        assert_eq!(doc["pcm"]["repositories"], json!([+            {"name": DEFAULT_REPOSITORY_NAME, "url": DEFAULT_REPOSITORY_URL},+            {"name": "Blind", "url": "https://example.test/none.json"},+            {"name": "Adom PCM", "url": REPO_URL},+        ]));+        assert_eq!(doc["meta"]["filename"], json!("kicad"));+        let text = std::fs::read_to_string(cfg.join("kicad.json")).unwrap();+        assert!(text.starts_with("{\n  \""), "two-space indent like KiCad: {text}");+        assert!(!cfg.join("kicad.json.adom-tmp").exists());+        // Idempotent: same URL again writes nothing (backup untouched).+        let before = std::fs::read_to_string(&bak).unwrap();+        let r = add_repository(&c, &json!({"url": REPO_URL, "name": "Renamed"}), &f);+        assert_eq!(r["added"], json!(false));+        assert_eq!(r["alreadyRegistered"], json!(true));+        assert_eq!(r["name"], json!("Adom PCM"));+        assert_eq!(std::fs::read_to_string(&bak).unwrap(), before);+        assert_eq!(registered_repositories(&c).len(), 3);+        // Other keys in kicad.json survive the edit.+        let mut doc = read_json(&cfg.join("kicad.json"));+        doc["system"] = json!({"check_for_kicad_updates": true});+        doc["pcm"]["lib_prefix"] = json!("PCM_");+        std::fs::write(cfg.join("kicad.json"), doc.to_string()).unwrap();+        let r = remove_repository(&c, &json!({"url": "https://example.test/none.json"}));+        assert_eq!(r["removed"], json!(true), "{r}");+        let doc = read_json(&cfg.join("kicad.json"));+        assert_eq!(doc["system"]["check_for_kicad_updates"], json!(true));+        assert_eq!(doc["pcm"]["lib_prefix"], json!("PCM_"));+        assert_eq!(doc["pcm"]["repositories"].as_array().unwrap().len(), 2);+        let r = remove_repository(&c, &json!({"url": "https://example.test/none.json"}));+        assert_eq!(r["removed"], json!(false));+        assert_eq!(remove_repository(&c, &json!({}))["errorCode"], json!("invalid_arg"));+        assert_eq!(add_repository(&Ctx::default(), &json!({"url": REPO_URL}), &f)["errorCode"], json!("settings_not_found"));+        // A corrupt kicad.json is refused, not clobbered.+        std::fs::write(cfg.join("kicad.json"), "{ nope").unwrap();+        assert_eq!(add_repository(&c, &json!({"url": "https://example.test/other.json", "name": "x"}), &f)["errorCode"], json!("settings_unreadable"));+        assert_eq!(std::fs::read_to_string(cfg.join("kicad.json")).unwrap(), "{ nope");+    }++    fn repo_table_v(versions: Vec<Value>) -> HashMap<String, Vec<u8>> {+        let packages = json!({"packages": [+            {"identifier": "inc.adom.kicad.test", "name": "Adom Test", "type": "library", "versions": versions}+        ]}).to_string().into_bytes();+        let repo = json!({"name": "Adom PCM", "packages": {"url": "https://example.test/packages.json", "sha256": sha256_hex(&packages)}}).to_string().into_bytes();+        let mut t = HashMap::new();+        t.insert(REPO_URL.to_string(), repo);+        t.insert("https://example.test/packages.json".to_string(), packages);+        t.insert("https://example.test/pkg.zip".to_string(), ZIP.to_vec());+        t+    }++    #[test]+    fn list_reports_updates_and_update_installs() {+        let t = tmp("update");+        let c = ctx(&t.0);+        let v100 = json!({"version": "1.0.0", "status": "stable", "kicad_version": "10.0", "download_url": "https://example.test/pkg.zip", "download_sha256": ZIP_SHA});+        let mut v110 = v100.clone();+        v110["version"] = json!("1.1.0");+        let mut v200 = v100.clone();+        v200["version"] = json!("2.0.0");+        v200["kicad_version"] = json!("11.0");++        // Repository offers 1.0.0 only; register it and install.+        let f1 = fetcher(repo_table_v(vec![v100.clone()]));+        assert_eq!(add_repository(&c, &json!({"url": REPO_URL}), &f1)["added"], json!(true));+        let r = install(&c, &json!({"repositoryUrl": REPO_URL, "identifier": "inc.adom.kicad.test"}), &f1);+        assert_eq!(r["success"], json!(true), "{r}");+        assert_eq!(r["version"], json!("1.0.0"));+        assert!(r["previousVersion"].is_null());+        assert_eq!(r["updated"], json!(false));+        let l = list(&c, &json!({}), &f1);+        assert_eq!(l["updatesAvailable"], json!(0));+        assert_eq!(l["packages"][0]["updateAvailable"], json!(false));+        assert_eq!(l["packages"][0]["latestVersion"], json!("1.0.0"));+        assert_eq!(l["packages"][0]["updateCheck"], json!("ok"));+        assert_eq!(l["packages"][0]["repositoryUrl"], json!(REPO_URL));+        assert_eq!(l["repositories"].as_array().unwrap().len(), 2);+        // update:true with nothing newer: upToDate, nothing touched.+        let r = install(&c, &json!({"identifier": "inc.adom.kicad.test", "update": true}), &f1);+        assert_eq!(r["upToDate"], json!(true), "{r}");+        assert_eq!(r["version"], json!("1.0.0"));++        // The repository bumps to 1.1.0 (and lists a 2.0.0 this KiCad cannot take).+        let f2 = fetcher(repo_table_v(vec![v100.clone(), v110.clone(), v200.clone()]));+        let l = list(&c, &json!({}), &f2);+        assert_eq!(l["updatesAvailable"], json!(1), "{l}");+        assert_eq!(l["packages"][0]["updateAvailable"], json!(true));+        assert_eq!(l["packages"][0]["latestVersion"], json!("1.1.0"), "2.0.0 needs KiCad 11, so it is not offered");+        assert!(l["_hint"].as_str().unwrap().contains("update"));+        // Headless Update button: repositoryUrl omitted, resolved from the registry row.+        let r = install(&c, &json!({"identifier": "inc.adom.kicad.test", "update": true}), &f2);+        assert_eq!(r["success"], json!(true), "{r}");+        assert_eq!(r["version"], json!("1.1.0"));+        assert_eq!(r["previousVersion"], json!("1.0.0"));+        assert_eq!(r["updated"], json!(true));+        assert_eq!(r["repositoryUrl"], json!(REPO_URL));+        assert!(r["_hint"].as_str().unwrap().contains("Replaced 1.0.0 with 1.1.0"));+        let reg = read_installed(&t.0.join("config/installed_packages.json"));+        assert_eq!(reg["packages"].as_array().unwrap().len(), 1, "one row, replaced");+        assert_eq!(reg["packages"][0]["current_version"], json!("1.1.0"));+        assert_eq!(reg["packages"][0]["package"]["versions"][0]["version"], json!("1.1.0"));+        let l = list(&c, &json!({}), &f2);+        assert_eq!(l["updatesAvailable"], json!(0));+        assert_eq!(l["packages"][0]["updateAvailable"], json!(false));+        // A pinned package is never offered an update, as PCM does.+        let mut reg = read_installed(&t.0.join("config/installed_packages.json"));+        reg["packages"][0]["current_version"] = json!("1.0.0");+        reg["packages"][0]["pinned"] = json!(true);+        write_installed(&t.0.join("config/installed_packages.json"), &reg).unwrap();+        let l = list(&c, &json!({}), &f2);+        assert_eq!(l["packages"][0]["updateAvailable"], json!(false));+        assert_eq!(l["packages"][0]["updateCheck"], json!("pinned"));+        assert_eq!(l["updatesAvailable"], json!(0));+        // Unreachable repository: the row says so instead of failing the verb.+        let l = list(&c, &json!({}), &fetcher(HashMap::new()));+        assert_eq!(l["success"], json!(true));+        assert!(l["packages"][0]["updateCheck"].as_str().unwrap().starts_with("fetch_failed"));+        // Repository removed from kicad.json: update:true without repositoryUrl cannot resolve it.+        remove_repository(&c, &json!({"url": REPO_URL}));+        let l = list(&c, &json!({}), &f2);+        assert_eq!(l["packages"][0]["updateCheck"], json!("repository_not_registered"));+        let r = install(&c, &json!({"identifier": "inc.adom.kicad.test", "update": true}), &f2);+        assert_eq!(r["errorCode"], json!("repository_not_registered"));+        // ...but an explicit repositoryUrl still works, and a not-installed package needs one.+        let r = install(&c, &json!({"identifier": "inc.adom.kicad.test", "update": true, "repositoryUrl": REPO_URL, "force": true}), &f2);+        assert_eq!(r["version"], json!("1.1.0"), "{r}");+        let r = install(&c, &json!({"identifier": "inc.adom.kicad.other", "update": true}), &f2);+        assert_eq!(r["errorCode"], json!("not_installed"));+    } }
rust/crates/kicad-core/tests/pcm_sample_lib.rsadded+95
@@ -0,0 +1,95 @@+//! The sample PCM package under pcm/src/adom-sample-lib (the wiki page acting as a KiCad+//! PCM repository, docs/pcm.md): the ten symbols and ten footprints parse with the same+//! structural checks kicad_lint_library runs, the packed archive under pcm/packages/+//! matches packages.json byte for byte (sha256, size), and the bridge's own zip reader+//! lays it out under the 3rdparty root exactly as PCM's extract() would.++use std::path::{Path, PathBuf};++use kicad_core::lint::{check_footprint_structure, check_symbol_lib_structure, parse_kicad_mod, parse_kicad_sym};+use kicad_core::pcm::{extract, sha256_hex};++fn repo(rel: &str) -> PathBuf {+    Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)+}++#[test]+fn sample_symbols_and_footprints_parse() {+    let sym_path = repo("pcm/src/adom-sample-lib/symbols/Adom_Sample.kicad_sym");+    let text = std::fs::read_to_string(&sym_path).expect("symbol library readable");+    check_symbol_lib_structure(&text).expect("balanced kicad_symbol_lib");+    let inv = parse_kicad_sym(&text);+    assert_eq!(inv["symbolCount"], 10, "{inv}");+    let names: Vec<&str> = inv["symbols"].as_array().unwrap().iter().map(|s| s["name"].as_str().unwrap()).collect();+    for n in ["AdomSample_R", "AdomSample_C", "AdomSample_L", "AdomSample_D", "AdomSample_LED", "AdomSample_C_Polarized", "AdomSample_D_Zener", "AdomSample_Fuse", "AdomSample_Crystal", "AdomSample_Battery_Cell"] {+        assert!(names.contains(&n), "missing {n} in {names:?}");+    }+    for s in inv["symbols"].as_array().unwrap() {+        assert!(s["pinCount"].as_u64().unwrap() >= 2, "{s}");+    }+    assert_eq!(inv["fileVersion"], 20251024, "KiCad 10 symbol format");+    assert!(!text.contains("(extends"), "every sample symbol is a base symbol");++    let pretty = repo("pcm/src/adom-sample-lib/footprints/Adom_Sample.pretty");+    let mut mods: Vec<PathBuf> = std::fs::read_dir(&pretty).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().map(|x| x == "kicad_mod").unwrap_or(false)).collect();+    mods.sort();+    assert_eq!(mods.len(), 10);+    for m in &mods {+        let t = std::fs::read_to_string(m).unwrap();+        check_footprint_structure(&t).unwrap_or_else(|e| panic!("{}: {e}", m.display()));+        let p = parse_kicad_mod(&t);+        let stem = m.file_stem().unwrap().to_string_lossy().to_string();+        assert_eq!(p["name"], serde_json::json!(stem), "declared name matches the file stem");+        assert!(stem.starts_with("AdomSample_"));+        assert!(p["padCount"].as_u64().unwrap() >= 2, "{}: {p}", m.display());+    }+}++#[test]+fn packed_archive_matches_packages_json_and_extracts_like_pcm() {+    let packages: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(repo("pcm/packages.json")).unwrap()).unwrap();+    let repository: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(repo("pcm/repository.json")).unwrap()).unwrap();+    // repository.json's digest of packages.json is the digest of the file on disk.+    let packages_bytes = std::fs::read(repo("pcm/packages.json")).unwrap();+    assert_eq!(repository["packages"]["sha256"], serde_json::json!(sha256_hex(&packages_bytes)));+    let pkg = &packages["packages"][0];+    assert_eq!(pkg["identifier"], "com.adom.kicad-bridge.sample-lib");+    let ident = pkg["identifier"].as_str().unwrap();+    for v in pkg["versions"].as_array().unwrap() {+        let url = v["download_url"].as_str().unwrap();+        let file = url.rsplit('/').next().unwrap();+        let zip = std::fs::read(repo("pcm/packages").join(file)).unwrap_or_else(|e| panic!("{file}: {e}"));+        assert_eq!(v["download_size"], serde_json::json!(zip.len()));+        assert_eq!(v["download_sha256"], serde_json::json!(sha256_hex(&zip)));+        let third = std::env::temp_dir().join(format!("kicad-core-pcm-sample-{}-{}", std::process::id(), v["version"].as_str().unwrap()));+        let _ = std::fs::remove_dir_all(&third);+        let e = extract(&zip, &third, ident).expect("the bridge's zip reader handles the packer's deflate output");+        assert_eq!(e.package_dir, "com_adom_kicad-bridge_sample-lib");+        assert_eq!(e.symbols, vec!["Adom_Sample"]);+        assert_eq!(e.footprints, vec!["Adom_Sample"]);+        assert_eq!(e.files, 12, "10 footprints + 1 symbol library + icon; metadata.json is not extracted");+        assert!(third.join("symbols/com_adom_kicad-bridge_sample-lib/Adom_Sample.kicad_sym").is_file());+        assert!(third.join("footprints/com_adom_kicad-bridge_sample-lib/Adom_Sample.pretty/AdomSample_R_0805_2012Metric.kicad_mod").is_file());+        assert!(third.join("resources/com_adom_kicad-bridge_sample-lib/icon.png").is_file());+        let mut total = 0u64;+        for f in walk(&third) {+            total += std::fs::metadata(&f).unwrap().len();+        }+        // install_size counts metadata.json too, which PCM does not extract.+        assert!(total < v["install_size"].as_u64().unwrap() && total > v["install_size"].as_u64().unwrap() - 4000, "{total} vs {}", v["install_size"]);+        let _ = std::fs::remove_dir_all(&third);+    }+}++fn walk(dir: &Path) -> Vec<PathBuf> {+    let mut out = Vec::new();+    for e in std::fs::read_dir(dir).unwrap().flatten() {+        let p = e.path();+        if p.is_dir() {+            out.extend(walk(&p));+        } else {+            out.push(p);+        }+    }+    out+}
tools/build_pcm.pyadded+435
@@ -0,0 +1,435 @@+#!/usr/bin/env python3+"""build_pcm.py: turn pcm/src/<package>/ into a KiCad PCM repository served from the wiki page.++What it writes (all under pcm/, all pushed to the page with `adom-wiki repo push`):++  pcm/packages/<identifier>-<version>.zip   the package archive, metadata.json at the root+  pcm/packages.json                          the PackageArray (every version, with download_*)+  pcm/repository.json                        the Repository (points at packages.json + its sha256)++Rules it enforces (KiCad's, see docs/pcm.md):+  * the archive's metadata.json carries ONLY the version being packed and NO download_* keys+    (KiCad's docs: a valid download_sha256 cannot exist inside the archive it hashes);+  * packages.json carries every version still on disk, so PCM sees an update when a newer+    version than the installed one is listed;+  * repository.json and packages.json validate against KiCad's pcm.v1.schema.json+    (vendored at pcm/pcm.v1.schema.json, fetched from the KiCad 10.0 branch);+  * the zip is deterministic (fixed timestamps, sorted entries), so re-running the packer+    with nothing changed rewrites nothing: same bytes, same sha256, same update_timestamp.++Usage:+  python3 tools/build_pcm.py                       build the newest version in metadata.json+  python3 tools/build_pcm.py --bump 1.1.0          add version 1.1.0 to metadata.json, then build it+  python3 tools/build_pcm.py --bump 1.1.0 --stamp  same, and stamp AdomSample_R's Value with the version+                                                    so the change is visible in the Symbol Editor+  python3 tools/build_pcm.py --check               validate only, write nothing++stdlib only. `jsonschema` is used for a second opinion when it is importable.+"""+from __future__ import annotations++import argparse+import hashlib+import io+import json+import re+import sys+import time+import zipfile+from pathlib import Path++HERE = Path(__file__).resolve().parent+REPO = HERE.parent+PCM = REPO / "pcm"+SRC = PCM / "src"+PACKAGES_DIR = PCM / "packages"+SCHEMA_PATH = PCM / "pcm.v1.schema.json"++DEFAULT_BASE_URL = "https://wiki.adom.inc/api/pages/adom/kicad-bridge/files/pcm"+REPOSITORY_NAME = "Adom KiCad Bridge sample PCM"+MAINTAINER = {"name": "Adom", "contact": {"web": "https://wiki.adom.inc/adom/kicad-bridge", "email": "[email protected]"}}++# PCM's extract() only knows these folders; anything else in the archive is dead weight.+CONTENT_FOLDERS = ("plugins", "footprints", "symbols", "3dmodels", "resources", "colors")+ZIP_DATE = (2026, 1, 1, 0, 0, 0)  # fixed so the archive bytes depend on content only+DOWNLOAD_KEYS = ("download_url", "download_sha256", "download_size", "install_size")+++# ── versions ─────────────────────────────────────────────────────────────────++def version_key(v: str) -> tuple:+    """major.minor.patch as ints, missing parts 0, like the bridge's compare_versions."""+    parts = []+    for p in v.split("."):+        try:+            parts.append(int(p))+        except ValueError:+            parts.append(0)+    while len(parts) < 3:+        parts.append(0)+    return tuple(parts)+++# ── mini JSON-schema validator (draft-07 subset used by pcm.v1.schema.json) ─────++class SchemaError(Exception):+    pass+++def _resolve(schema: dict, ref: str) -> dict:+    assert ref.startswith("#/"), ref+    node = schema+    for part in ref[2:].split("/"):+        node = node[part]+    return node+++def validate(schema: dict, node: dict, data, path: str = "$") -> None:+    """Enough of draft-07 to check this schema: $ref, type, required, properties,+    additionalProperties, patternProperties, pattern, enum, items, minItems, uniqueItems,+    minimum, maxLength. Raises SchemaError with the JSON path of the first problem."""+    if "$ref" in node:+        node = _resolve(schema, node["$ref"])+    t = node.get("type")+    if t:+        ok = {"object": lambda d: isinstance(d, dict), "array": lambda d: isinstance(d, list),+              "string": lambda d: isinstance(d, str),+              "integer": lambda d: isinstance(d, int) and not isinstance(d, bool)}[t](data)+        if not ok:+            raise SchemaError(f"{path}: expected {t}, got {type(data).__name__}")+    if "enum" in node and data not in node["enum"]:+        raise SchemaError(f"{path}: {data!r} is not one of the allowed values")+    if isinstance(data, str):+        if "pattern" in node and not re.search(node["pattern"], data):+            raise SchemaError(f"{path}: {data!r} does not match {node['pattern']}")+        if "maxLength" in node and len(data) > node["maxLength"]:+            raise SchemaError(f"{path}: longer than {node['maxLength']} characters")+    if isinstance(data, int) and not isinstance(data, bool) and "minimum" in node and data < node["minimum"]:+        raise SchemaError(f"{path}: {data} is below {node['minimum']}")+    if isinstance(data, dict):+        for r in node.get("required", []):+            if r not in data:+                raise SchemaError(f"{path}: missing required key {r!r}")+        props = node.get("properties", {})+        pprops = node.get("patternProperties", {})+        for k, v in data.items():+            if k in props:+                validate(schema, props[k], v, f"{path}.{k}")+                continue+            matched = False+            for pat, sub in pprops.items():+                if re.search(pat, k):+                    matched = True+                    validate(schema, sub, v, f"{path}.{k}")+            if not matched and node.get("additionalProperties") is False:+                raise SchemaError(f"{path}: key {k!r} is not allowed here")+    if isinstance(data, list):+        if "minItems" in node and len(data) < node["minItems"]:+            raise SchemaError(f"{path}: fewer than {node['minItems']} items")+        if node.get("uniqueItems"):+            seen = [json.dumps(x, sort_keys=True) for x in data]+            if len(seen) != len(set(seen)):+                raise SchemaError(f"{path}: items are not unique")+        if "items" in node:+            for i, item in enumerate(data):+                validate(schema, node["items"], item, f"{path}[{i}]")+++def check_against_schema(schema: dict, definition: str, data, label: str) -> None:+    validate(schema, {"$ref": f"#/definitions/{definition}"}, data, label)+    try:+        import jsonschema  # type: ignore+    except ImportError:+        return+    sub = dict(schema)+    sub["$ref"] = f"#/definitions/{definition}"+    jsonschema.validate(data, sub)  # raises jsonschema.ValidationError+++# ── KiCad file structure checks (mirror kicad_core::lint) ─────────────────────++def quote_aware_top_children(text: str) -> tuple[str, list[str]]:+    """(root head token, direct child heads) of the first top-level s-expression; raises on imbalance."""+    depth = 0+    in_s = esc = False+    root_open = None+    child_start = None+    children = []+    for i, ch in enumerate(text):+        if in_s:+            if esc:+                esc = False+            elif ch == "\\":+                esc = True+            elif ch == '"':+                in_s = False+            continue+        if ch == '"':+            in_s = True+        elif ch == "(":+            depth += 1+            if depth == 1:+                root_open = i+            elif depth == 2 and child_start is None:+                child_start = i+        elif ch == ")":+            depth -= 1+            if depth == 1 and child_start is not None:+                children.append(text[child_start:i + 1])+                child_start = None+            elif depth == 0:+                if text[i + 1:].strip():+                    raise ValueError("trailing content after the closing paren")+                head = text[root_open + 1:].split(None, 1)[0]+                return head, children+            elif depth < 0:+                raise ValueError(f"extra ')' at offset {i}")+    raise ValueError("unbalanced s-expression")+++def check_symbol_lib(path: Path) -> list[str]:+    head, children = quote_aware_top_children(path.read_text(encoding="utf-8"))+    if head != "kicad_symbol_lib":+        raise ValueError(f"{path.name}: not a symbol library ({head})")+    names = []+    for c in children:+        m = re.match(r'\(symbol\s+"([^"]+)"', c)+        if m:+            names.append(m.group(1))+    return names+++def check_footprint(path: Path) -> str:+    text = path.read_text(encoding="utf-8")+    head, _ = quote_aware_top_children(text)+    if head != "footprint":+        raise ValueError(f"{path.name}: not a footprint ({head})")+    m = re.match(r'\(footprint\s+"([^"]+)"', text)+    name = m.group(1) if m else ""+    if name != path.stem:+        raise ValueError(f"{path.name}: declares footprint {name!r}, file stem is {path.stem!r}")+    return name+++def check_package_tree(pkg_dir: Path) -> dict:+    """Layout per dev-docs.kicad.org/en/addons: metadata.json at the root, content folders only."""+    meta_path = pkg_dir / "metadata.json"+    if not meta_path.is_file():+        raise ValueError(f"{pkg_dir}: no metadata.json")+    report = {"symbols": {}, "footprints": {}, "extra": []}+    for child in sorted(pkg_dir.iterdir()):+        if child.name == "metadata.json":+            continue+        if not child.is_dir() or child.name not in CONTENT_FOLDERS:+            report["extra"].append(child.name)+    if report["extra"]:+        raise ValueError(f"{pkg_dir}: entries PCM would ignore: {report['extra']} (only {CONTENT_FOLDERS} and metadata.json belong in a package)")+    for sym in sorted((pkg_dir / "symbols").glob("*.kicad_sym")) if (pkg_dir / "symbols").is_dir() else []:+        report["symbols"][sym.name] = check_symbol_lib(sym)+    if (pkg_dir / "footprints").is_dir():+        for pretty in sorted((pkg_dir / "footprints").glob("*.pretty")):+            report["footprints"][pretty.name] = [check_footprint(m) for m in sorted(pretty.glob("*.kicad_mod"))]+    icon = pkg_dir / "resources" / "icon.png"+    if icon.is_file():+        head = icon.read_bytes()[:24]+        if head[:8] != b"\x89PNG\r\n\x1a\n":+            raise ValueError("resources/icon.png is not a PNG")+        w, h = int.from_bytes(head[16:20], "big"), int.from_bytes(head[20:24], "big")+        if (w, h) != (64, 64):+            raise ValueError(f"resources/icon.png is {w}x{h}; PCM wants 64x64")+    return report+++# ── the archive ───────────────────────────────────────────────────────────────++def build_zip(pkg_dir: Path, meta: dict, version: dict) -> tuple[bytes, int]:+    """Deterministic zip: metadata.json (this version only, no download_*) at the root,+    then every file under the content folders, sorted. Returns (bytes, install_size)."""+    archive_meta = {k: v for k, v in meta.items() if k != "versions"}+    archive_meta["versions"] = [{k: v for k, v in version.items() if k not in DOWNLOAD_KEYS}]+    meta_bytes = (json.dumps(archive_meta, indent=4, ensure_ascii=False) + "\n").encode("utf-8")+    files: list[tuple[str, bytes]] = [("metadata.json", meta_bytes)]+    for folder in CONTENT_FOLDERS:+        d = pkg_dir / folder+        if not d.is_dir():+            continue+        for f in sorted(p for p in d.rglob("*") if p.is_file()):+            files.append((f.relative_to(pkg_dir).as_posix(), f.read_bytes()))+    buf = io.BytesIO()+    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:+        for name, data in files:+            zi = zipfile.ZipInfo(name, date_time=ZIP_DATE)+            zi.compress_type = zipfile.ZIP_DEFLATED+            zi.external_attr = 0o644 << 16+            zf.writestr(zi, data)+    return buf.getvalue(), sum(len(d) for _, d in files)+++def write_if_changed(path: Path, data: bytes) -> bool:+    if path.is_file() and path.read_bytes() == data:+        return False+    path.parent.mkdir(parents=True, exist_ok=True)+    path.write_bytes(data)+    return True+++def dump_json(obj) -> bytes:+    return (json.dumps(obj, indent=4, ensure_ascii=False) + "\n").encode("utf-8")+++# ── main ──────────────────────────────────────────────────────────────────────++def stamp_symbol(pkg_dir: Path, symbol: str, version: str) -> None:+    """Make the bump visible: set the symbol's Value property to '<base> v<version>'."""+    for sym in (pkg_dir / "symbols").glob("*.kicad_sym"):+        text = sym.read_text(encoding="utf-8")+        start = text.find(f'(symbol "{symbol}"')+        if start < 0:+            continue+        m = re.compile(r'\(property "Value" "([^"]*)"').search(text, start)+        if not m:+            raise ValueError(f"{symbol}: no Value property")+        base = re.sub(r" v\d[\d.]*$", "", m.group(1))+        text = text[:m.start()] + f'(property "Value" "{base} v{version}"' + text[m.end():]+        sym.write_text(text, encoding="utf-8")+        print(f"  stamped {symbol} Value = {base!r} v{version} in {sym.name}")+        return+    raise ValueError(f"symbol {symbol} not found under {pkg_dir}/symbols")+++def main() -> int:+    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)+    ap.add_argument("--package", default="adom-sample-lib", help="folder under pcm/src (default adom-sample-lib)")+    ap.add_argument("--bump", metavar="X.Y.Z", help="append this version to metadata.json before building")+    ap.add_argument("--stamp", action="store_true", help="with --bump: stamp AdomSample_R's Value with the new version")+    ap.add_argument("--stamp-symbol", default="AdomSample_R")+    ap.add_argument("--status", default="stable", choices=["stable", "testing", "development", "deprecated"])+    ap.add_argument("--kicad-version", default="10.0")+    ap.add_argument("--base-url", default=DEFAULT_BASE_URL, help="where pcm/ is served from (page file URL)")+    ap.add_argument("--check", action="store_true", help="validate the source and the current outputs; write nothing")+    args = ap.parse_args()++    pkg_dir = SRC / args.package+    meta_path = pkg_dir / "metadata.json"+    schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))+    meta = json.loads(meta_path.read_text(encoding="utf-8"))+    base_url = args.base_url.rstrip("/")++    if args.bump:+        if not re.fullmatch(r"\d{1,4}(\.\d{1,4}(\.\d{1,6})?)?", args.bump):+            print(f"--bump {args.bump}: not a PCM version (major[.minor[.patch]])", file=sys.stderr)+            return 2+        existing = [v["version"] for v in meta["versions"]]+        if args.bump in existing:+            print(f"--bump {args.bump}: already in metadata.json ({existing})", file=sys.stderr)+            return 2+        if any(version_key(args.bump) <= version_key(v) for v in existing):+            print(f"--bump {args.bump}: not newer than {max(existing, key=version_key)}", file=sys.stderr)+            return 2+        if args.check:+            print("--check with --bump: nothing written", file=sys.stderr)+            return 2+        meta["versions"].append({"version": args.bump, "status": args.status, "kicad_version": args.kicad_version})+        meta["versions"].sort(key=lambda v: version_key(v["version"]))+        meta_path.write_bytes(dump_json(meta))+        print(f"  metadata.json: added version {args.bump}")+        if args.stamp:+            stamp_symbol(pkg_dir, args.stamp_symbol, args.bump)++    # 1. Source checks.+    report = check_package_tree(pkg_dir)+    for lib, names in report["symbols"].items():+        print(f"  symbols/{lib}: {len(names)} symbols: {', '.join(names)}")+    for lib, names in report["footprints"].items():+        print(f"  footprints/{lib}: {len(names)} footprints")+    check_against_schema(schema, "Package", meta, "metadata.json")+    if len(meta["description"]) > 150:+        raise ValueError("description is over 150 characters (the PCM dialog limit in KiCad's docs)")+    for v in meta["versions"]:+        for k in DOWNLOAD_KEYS:+            if k in v:+                raise ValueError(f"metadata.json versions[{v['version']}] carries {k}: the packer computes download_* itself")++    ident = meta["identifier"]+    versions = sorted(meta["versions"], key=lambda v: version_key(v["version"]))+    newest = versions[-1]++    # 2. The archive for the newest version (older versions keep the zip already on disk).+    zip_bytes, install_size = build_zip(pkg_dir, meta, newest)+    zip_name = f"{ident}-{newest['version']}.zip"+    zip_path = PACKAGES_DIR / zip_name+    if not args.check:+        changed = write_if_changed(zip_path, zip_bytes)+        print(f"  {zip_path.relative_to(REPO)}: {len(zip_bytes)} bytes, install_size {install_size}{' (unchanged)' if not changed else ''}")++    # 3. packages.json: every version whose zip exists, download_* computed from the bytes.+    old_packages = {}+    if (PCM / "packages.json").is_file():+        for p in json.loads((PCM / "packages.json").read_text(encoding="utf-8")).get("packages", []):+            old_packages[p["identifier"]] = p+    published_versions = []+    for v in versions:+        zp = PACKAGES_DIR / f"{ident}-{v['version']}.zip"+        if v is newest:+            data, isize = zip_bytes, install_size+        elif zp.is_file():+            data = zp.read_bytes()+            with zipfile.ZipFile(io.BytesIO(data)) as zf:+                isize = sum(i.file_size for i in zf.infolist())+        else:+            print(f"  WARNING: {zp.name} is not on disk; version {v['version']} is dropped from packages.json", file=sys.stderr)+            continue+        entry = {k: val for k, val in v.items() if k not in DOWNLOAD_KEYS}+        entry.update({+            "download_url": f"{base_url}/packages/{zp.name}",+            "download_sha256": hashlib.sha256(data).hexdigest(),+            "download_size": len(data),+            "install_size": isize,+        })+        published_versions.append(entry)+    package_entry = {k: v for k, v in meta.items() if k not in ("versions", "$schema")}+    package_entry["versions"] = published_versions+    # Other packages already in packages.json (a second pcm/src folder built earlier) are kept.+    packages = [p for i, p in old_packages.items() if i != ident] + [package_entry]+    packages_doc = {"packages": packages}+    check_against_schema(schema, "PackageArray", packages_doc, "packages.json")+    packages_bytes = dump_json(packages_doc)++    # 4. repository.json: keep update_timestamp when packages.json did not change.+    old_repo = json.loads((PCM / "repository.json").read_text(encoding="utf-8")) if (PCM / "repository.json").is_file() else {}+    sha = hashlib.sha256(packages_bytes).hexdigest()+    if old_repo.get("packages", {}).get("sha256") == sha and old_repo["packages"].get("url") == f"{base_url}/packages.json":+        ts = int(old_repo["packages"]["update_timestamp"])+    else:+        ts = int(time.time())+    repo_doc = {+        "$schema": "https://go.kicad.org/pcm/schemas/v1",+        "name": REPOSITORY_NAME,+        "maintainer": MAINTAINER,+        "packages": {+            "url": f"{base_url}/packages.json",+            "sha256": sha,+            "update_timestamp": ts,+            "update_time_utc": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts)),+        },+    }+    check_against_schema(schema, "Repository", repo_doc, "repository.json")++    if args.check:+        cur = (PCM / "packages.json").read_bytes() if (PCM / "packages.json").is_file() else b""+        print("  packages.json:", "up to date" if cur == packages_bytes else "WOULD CHANGE")+        return 0 if cur == packages_bytes else 1+    c1 = write_if_changed(PCM / "packages.json", packages_bytes)+    c2 = write_if_changed(PCM / "repository.json", dump_json(repo_doc))+    print(f"  pcm/packages.json: {len(published_versions)} version(s) of {ident}{' (unchanged)' if not c1 else ''}")+    print(f"  pcm/repository.json: packages sha256 {sha[:16]}..., update_timestamp {ts}{' (unchanged)' if not c2 else ''}")+    print(f"  repository URL for KiCad: {base_url}/repository.json")+    return 0+++if __name__ == "__main__":+    try:+        sys.exit(main())+    except (ValueError, SchemaError) as e:+        print(f"build_pcm: {e}", file=sys.stderr)+        sys.exit(1)