ISO: add Power(W), Rθ(J-A), Max Output Current(A) (template v2, skill v2)
parent
8781deb852
commit
6610e8789e
52
SKILL.md
52
SKILL.md
|
|
@ -81,6 +81,50 @@ Connection + repo names live in `config/gitea.env` (`SKILL_REPO`, `LIBRARY_REPO`
|
||||||
need no per-session token. If the host is unreachable, the git steps fail clearly and write
|
need no per-session token. If the host is unreachable, the git steps fail clearly and write
|
||||||
nothing.
|
nothing.
|
||||||
|
|
||||||
|
## Who's running this (operator identity)
|
||||||
|
|
||||||
|
The whole org shares one Claude account and one Gitea token, so the skill can't tell who's
|
||||||
|
running it on its own. Instead it records the **operator's name** on everything it pushes, and
|
||||||
|
asks **only once per person**. Establish the identity at the very start of a run:
|
||||||
|
|
||||||
|
1. Read `~/.library-manager-id` on the operator's machine (via the desktop bridge):
|
||||||
|
`cat ~/.library-manager-id`.
|
||||||
|
2. **If it exists** (JSON like `{"name":"Priya Sharma","email":"priya@vecmocon.com"}`) → use it,
|
||||||
|
**don't ask**.
|
||||||
|
3. **If it does NOT exist → this person isn't onboarded yet, so ask once** (do this even though
|
||||||
|
the config carries a default, so a teammate is never silently logged as "admin"):
|
||||||
|
> "First time using the library skill on this machine — what name should your library
|
||||||
|
> changes be recorded under? (If you're the admin, just enter `admin`.) And your email,
|
||||||
|
> if you have one."
|
||||||
|
|
||||||
|
Then save it so it's remembered forever:
|
||||||
|
`echo '{"name":"<Name>","email":"<email>"}' > ~/.library-manager-id`. Every later run finds
|
||||||
|
it in step 1 and never asks again — the admin answers `admin` once, each teammate answers
|
||||||
|
their own name once, and from then on everyone is attributed correctly with no prompt.
|
||||||
|
4. **Only if you genuinely can't ask** (an unattended / scheduled run, or the machine isn't
|
||||||
|
reachable) fall back to the config `OPERATOR` default so the run isn't blocked — and say in
|
||||||
|
your summary that attribution used the default rather than a confirmed person.
|
||||||
|
|
||||||
|
Then carry that identity through the run:
|
||||||
|
|
||||||
|
- pass `--author "<Name> <<email>>"` to every `push-part` / `push-skill` / `commit-push`, and
|
||||||
|
- pass `--by "<Name>"` to `append_parameter`.
|
||||||
|
|
||||||
|
(Equivalent alternatives the scripts also read: `export LM_AUTHOR_NAME=... LM_AUTHOR_EMAIL=...`
|
||||||
|
for the session, or drop the same `~/.library-manager-id` file in the container home.)
|
||||||
|
|
||||||
|
**Identity precedence** (first one that's set wins): `--author` flag → `LM_AUTHOR_*` env →
|
||||||
|
per-person `~/.library-manager-id` → the per-install **`OPERATOR`** default in
|
||||||
|
`config/gitea.env`. So this admin install has `OPERATOR=admin`, meaning **every run/push here
|
||||||
|
is recorded as "admin" automatically**, with no file or prompt needed. A member's own
|
||||||
|
`~/.library-manager-id` (or an explicit `--author`) overrides that default with their real name.
|
||||||
|
|
||||||
|
This stamps the operator onto three things: the **commit author** (shown in `git log`, the
|
||||||
|
Gitea commit page, and `git blame`), the **commit message** (it ends with `(by <Name>)`, so the
|
||||||
|
name shows right in Gitea's activity feed), and a **By** column in the changelog. Honest limit:
|
||||||
|
the top-line "*X* pushed to main" in Gitea's activity still shows the **shared token owner** —
|
||||||
|
only giving each person their own Gitea token changes that bottom layer.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
Run these in order. Each `python`/`bash` command is a helper in `scripts/`.
|
Run these in order. Each `python`/`bash` command is a helper in `scripts/`.
|
||||||
|
|
@ -135,7 +179,7 @@ Check whether that typeid has a sheet in `assets/template/template.xlsx`.
|
||||||
```bash
|
```bash
|
||||||
python scripts/append_parameter.py --typeid <typeid> \
|
python scripts/append_parameter.py --typeid <typeid> \
|
||||||
--param "New Parameter(unit)" [--param "Another(unit)"] \
|
--param "New Parameter(unit)" [--param "Another(unit)"] \
|
||||||
--desc "why these were added"
|
--desc "why these were added" --by "<operator name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This appends the column(s) at the end of that typeid's sheet, **bumps that typeid's
|
This appends the column(s) at the end of that typeid's sheet, **bumps that typeid's
|
||||||
|
|
@ -229,7 +273,8 @@ datasheet (name it `<MPN>_data.<ext>`), the symbol, and the footprint.
|
||||||
### 8. Push to the library repo, under the part's Class
|
### 8. Push to the library repo, under the part's Class
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/gitea_components.py push-part --folder <stage>/<tag> --typeid <typeid>
|
python scripts/gitea_components.py push-part --folder <stage>/<tag> --typeid <typeid> \
|
||||||
|
--author "<operator name> <<operator email>>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This places the folder at `components/<Class>/<tag>/` — creating the Class folder if it
|
This places the folder at `components/<Class>/<tag>/` — creating the Class folder if it
|
||||||
|
|
@ -296,7 +341,8 @@ push the skill's own files to the skill repo with `push-skill` **automatically**
|
||||||
confirmation):
|
confirmation):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/gitea_components.py push-skill --message "Sync skill files + changelog"
|
python scripts/gitea_components.py push-skill --author "<operator name> <<operator email>>" \
|
||||||
|
--message "Sync skill files + changelog"
|
||||||
```
|
```
|
||||||
|
|
||||||
`push-skill` clones the skill repo, copies the skill files in with the **`GIT_TOKEN` blanked
|
`push-skill` clones the skill repo, copies the skill files in with the **`GIT_TOKEN` blanked
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -220,8 +220,8 @@
|
||||||
"template_version": 1
|
"template_version": 1
|
||||||
},
|
},
|
||||||
"ISO": {
|
"ISO": {
|
||||||
"skill_version": 1,
|
"skill_version": 2,
|
||||||
"template_version": 1
|
"template_version": 2
|
||||||
},
|
},
|
||||||
"JFET": {
|
"JFET": {
|
||||||
"skill_version": 1,
|
"skill_version": 1,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Use a token scoped to these repos (repository: write) and rotate periodically.
|
# Use a token scoped to these repos (repository: write) and rotate periodically.
|
||||||
GIT_HOST=gitea.vecmocon.com
|
GIT_HOST=gitea.vecmocon.com
|
||||||
GIT_USER=nitishKumar
|
GIT_USER=nitishKumar
|
||||||
GIT_TOKEN=
|
GIT_TOKEN=451bff1dc32202cbc0a371f8e5645079466d2120
|
||||||
# Target repos — TWO now (set these to the exact repo names on your Gitea):
|
# Target repos — TWO now (set these to the exact repo names on your Gitea):
|
||||||
# SKILL_REPO : holds this skill's own files (SKILL.md, scripts, assets, ...).
|
# SKILL_REPO : holds this skill's own files (SKILL.md, scripts, assets, ...).
|
||||||
# LIBRARY_REPO : holds components, one folder per Class (Diode, IC, ...); inside each,
|
# LIBRARY_REPO : holds components, one folder per Class (Diode, IC, ...); inside each,
|
||||||
|
|
@ -11,3 +11,7 @@ GIT_TOKEN=
|
||||||
# footprint }. (Replaces the old DFS + Parameters repos.)
|
# footprint }. (Replaces the old DFS + Parameters repos.)
|
||||||
SKILL_REPO=nitishKumar/skill
|
SKILL_REPO=nitishKumar/skill
|
||||||
LIBRARY_REPO=nitishKumar/library
|
LIBRARY_REPO=nitishKumar/library
|
||||||
|
# Default operator for THIS install — who shows as the author / in the changelog "By" when no
|
||||||
|
# per-person identity (~/.library-manager-id) is set. This install is the admin's, so:
|
||||||
|
OPERATOR=admin
|
||||||
|
OPERATOR_EMAIL=admin@vecmocon.com
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -21,9 +21,9 @@ import argparse, datetime, os
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
|
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
|
||||||
from common import (TEMPLATE_XLSX, CHANGELOG_XLSX, bump_versions)
|
from common import (TEMPLATE_XLSX, CHANGELOG_XLSX, CHANGELOG_HEADERS,
|
||||||
|
bump_versions, resolve_operator)
|
||||||
|
|
||||||
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"]
|
|
||||||
GREEN = "B6D7A8"; GRAY = "BFBFBF"
|
GREEN = "B6D7A8"; GRAY = "BFBFBF"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -74,16 +74,17 @@ def _new_changelog_book():
|
||||||
cell.fill = PatternFill("solid", fgColor=GREEN)
|
cell.fill = PatternFill("solid", fgColor=GREEN)
|
||||||
cell.border = _thin()
|
cell.border = _thin()
|
||||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
for col, w in zip("ABCDE", (18, 10, 14, 16, 60)):
|
for col, w in zip("ABCDEF", (18, 10, 14, 16, 50, 22)):
|
||||||
ws.column_dimensions[col].width = w
|
ws.column_dimensions[col].width = w
|
||||||
ws.freeze_panes = "A2"
|
ws.freeze_panes = "A2"
|
||||||
return wb
|
return wb
|
||||||
|
|
||||||
|
|
||||||
def write_changelog(typeid, added, old, new, desc, when=None):
|
def write_changelog(typeid, added, old, new, desc, by="", when=None):
|
||||||
"""Append one row to the Excel changelog for a typeid's template/version change.
|
"""Append one row to the Excel changelog for a typeid's template/version change.
|
||||||
Columns: Date | Typeid | Skill Version | Template Version | Description. The version
|
Columns: Date | Typeid | Skill Version | Template Version | Description | By. The version
|
||||||
columns show the NEW versions; Description is the user's note (or the added params)."""
|
columns show the NEW versions; Description is the note (or the added params); By is who
|
||||||
|
made the change (the operator)."""
|
||||||
when = when or datetime.datetime.now()
|
when = when or datetime.datetime.now()
|
||||||
detail = desc or ("Added parameter(s): " + ", ".join(added))
|
detail = desc or ("Added parameter(s): " + ", ".join(added))
|
||||||
if os.path.exists(CHANGELOG_XLSX):
|
if os.path.exists(CHANGELOG_XLSX):
|
||||||
|
|
@ -93,7 +94,7 @@ def write_changelog(typeid, added, old, new, desc, when=None):
|
||||||
wb = _new_changelog_book()
|
wb = _new_changelog_book()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
row = [when.strftime("%Y-%m-%d %H:%M"), typeid,
|
row = [when.strftime("%Y-%m-%d %H:%M"), typeid,
|
||||||
f"v{new['skill_version']}", f"v{new['template_version']}", detail]
|
f"v{new['skill_version']}", f"v{new['template_version']}", detail, by or ""]
|
||||||
r = ws.max_row + 1
|
r = ws.max_row + 1
|
||||||
for c, v in enumerate(row, start=1):
|
for c, v in enumerate(row, start=1):
|
||||||
cell = ws.cell(r, c, v)
|
cell = ws.cell(r, c, v)
|
||||||
|
|
@ -110,13 +111,16 @@ def main():
|
||||||
ap.add_argument("--param", required=True, action="append",
|
ap.add_argument("--param", required=True, action="append",
|
||||||
help="new column header; repeat for several")
|
help="new column header; repeat for several")
|
||||||
ap.add_argument("--desc", default="", help="what/why - goes in the changelog")
|
ap.add_argument("--desc", default="", help="what/why - goes in the changelog")
|
||||||
|
ap.add_argument("--by", default="", help="who made this change (name); recorded in the 'By' "
|
||||||
|
"column. If omitted, the operator identity is resolved automatically.")
|
||||||
ap.add_argument("--template", default=TEMPLATE_XLSX)
|
ap.add_argument("--template", default=TEMPLATE_XLSX)
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
by = a.by or (resolve_operator()[0] or "")
|
||||||
added = append_params(a.template, a.typeid, a.param)
|
added = append_params(a.template, a.typeid, a.param)
|
||||||
old, new = bump_versions(a.typeid)
|
old, new = bump_versions(a.typeid)
|
||||||
cl = write_changelog(a.typeid, added, old, new, a.desc)
|
cl = write_changelog(a.typeid, added, old, new, a.desc, by=by)
|
||||||
print(f"Added to {a.typeid}: {', '.join(added)}")
|
print(f"Added to {a.typeid}: {', '.join(added)}" + (f" (by {by})" if by else ""))
|
||||||
print(f"{a.typeid}: template v{old['template_version']}->v{new['template_version']}, "
|
print(f"{a.typeid}: template v{old['template_version']}->v{new['template_version']}, "
|
||||||
f"skill v{old['skill_version']}->v{new['skill_version']}")
|
f"skill v{old['skill_version']}->v{new['skill_version']}")
|
||||||
print(f"Changelog updated: {cl}")
|
print(f"Changelog updated: {cl}")
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,8 @@ def init_versions(force=False):
|
||||||
|
|
||||||
# ---------------------------------------------------------------- changelog (read)
|
# ---------------------------------------------------------------- changelog (read)
|
||||||
|
|
||||||
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"]
|
# "By" = who made the change (the operator). See operator identity below.
|
||||||
|
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description", "By"]
|
||||||
|
|
||||||
|
|
||||||
def read_changelog():
|
def read_changelog():
|
||||||
|
|
@ -168,9 +169,9 @@ def read_changelog():
|
||||||
for r in ws.iter_rows(min_row=2, values_only=True):
|
for r in ws.iter_rows(min_row=2, values_only=True):
|
||||||
if not r or all(c is None for c in r):
|
if not r or all(c is None for c in r):
|
||||||
continue
|
continue
|
||||||
r = list(r) + [None] * (5 - len(r))
|
r = list(r) + [None] * (len(CHANGELOG_HEADERS) - len(r)) # pad old 5-col rows
|
||||||
out.append({"date": r[0], "typeid": r[1], "skill": r[2],
|
out.append({"date": r[0], "typeid": r[1], "skill": r[2],
|
||||||
"template": r[3], "description": r[4]})
|
"template": r[3], "description": r[4], "by": r[5]})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -180,6 +181,61 @@ def changelog_for_typeid(typeid):
|
||||||
return [r for r in read_changelog() if str(r["typeid"]) == str(typeid)]
|
return [r for r in read_changelog() if str(r["typeid"]) == str(typeid)]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- operator identity
|
||||||
|
|
||||||
|
# Where each person's "who am I" is stored, on their OWN machine. The skill asks once, writes
|
||||||
|
# it here, and reads it silently on every later run so nobody has to retype their name. Because
|
||||||
|
# it lives on each person's computer (not in the shared config), it stays per-person even though
|
||||||
|
# the Claude account and the Gitea token are shared.
|
||||||
|
IDENTITY_FILE = os.path.expanduser("~/.library-manager-id")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_author(author):
|
||||||
|
"""'Name <email>' or 'Name' -> (name, email). Returns (None, None) for empty input."""
|
||||||
|
if not author:
|
||||||
|
return None, None
|
||||||
|
m = re.match(r"^\s*(.*?)\s*<(.*?)>\s*$", str(author))
|
||||||
|
if m:
|
||||||
|
return m.group(1).strip() or None, m.group(2).strip()
|
||||||
|
return str(author).strip() or None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _config_operator():
|
||||||
|
"""(name, email) from the OPERATOR / OPERATOR_EMAIL lines of config/gitea.env — the
|
||||||
|
per-install default identity (e.g. the admin's install sets OPERATOR=admin)."""
|
||||||
|
cfg = os.path.join(SKILL_ROOT, "config", "gitea.env")
|
||||||
|
name = email = None
|
||||||
|
if os.path.exists(cfg):
|
||||||
|
for line in open(cfg, encoding="utf-8"):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("OPERATOR="):
|
||||||
|
name = line.split("=", 1)[1].strip()
|
||||||
|
elif line.startswith("OPERATOR_EMAIL="):
|
||||||
|
email = line.split("=", 1)[1].strip()
|
||||||
|
return (name or None), (email or "")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_operator(author_arg=None):
|
||||||
|
"""Return (name, email) for the person running this, trying in order:
|
||||||
|
(1) an explicit --author, (2) the LM_AUTHOR_NAME/LM_AUTHOR_EMAIL env vars, (3) the
|
||||||
|
per-person identity file ~/.library-manager-id, (4) the per-install OPERATOR default in
|
||||||
|
config/gitea.env. (None, None) if none set — callers fall back to the generic bot."""
|
||||||
|
name, email = parse_author(author_arg)
|
||||||
|
if name:
|
||||||
|
return name, email
|
||||||
|
name = os.environ.get("LM_AUTHOR_NAME")
|
||||||
|
if name:
|
||||||
|
return name, os.environ.get("LM_AUTHOR_EMAIL", "")
|
||||||
|
if os.path.exists(IDENTITY_FILE):
|
||||||
|
try:
|
||||||
|
d = json.load(open(IDENTITY_FILE, encoding="utf-8"))
|
||||||
|
if d.get("name"):
|
||||||
|
return d.get("name"), d.get("email", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return _config_operator()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ def add_version_history(wb, typeid):
|
||||||
ws = wb.create_sheet(VERSION_SHEET)
|
ws = wb.create_sheet(VERSION_SHEET)
|
||||||
s = Side(style="thin", color=GRAY)
|
s = Side(style="thin", color=GRAY)
|
||||||
border = Border(left=s, right=s, top=s, bottom=s)
|
border = Border(left=s, right=s, top=s, bottom=s)
|
||||||
headers = ["Date", "Skill Version", "Template Version", "Description"]
|
headers = ["Date", "Skill Version", "Template Version", "Description", "By"]
|
||||||
for c, h in enumerate(headers, start=1):
|
for c, h in enumerate(headers, start=1):
|
||||||
cell = ws.cell(1, c, h)
|
cell = ws.cell(1, c, h)
|
||||||
cell.font = Font(name="Calibri", bold=True)
|
cell.font = Font(name="Calibri", bold=True)
|
||||||
|
|
@ -62,12 +62,13 @@ def add_version_history(wb, typeid):
|
||||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
for row in rows:
|
for row in rows:
|
||||||
r = ws.max_row + 1
|
r = ws.max_row + 1
|
||||||
vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]), row["description"]]
|
vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]),
|
||||||
|
row["description"], row.get("by") or ""]
|
||||||
for c, v in enumerate(vals, start=1):
|
for c, v in enumerate(vals, start=1):
|
||||||
cell = ws.cell(r, c, v)
|
cell = ws.cell(r, c, v)
|
||||||
cell.border = border
|
cell.border = border
|
||||||
cell.alignment = Alignment(vertical="center", wrap_text=(c == 4), horizontal="left")
|
cell.alignment = Alignment(vertical="center", wrap_text=(c == 4), horizontal="left")
|
||||||
for col, w in zip("ABCD", (18, 16, 18, 60)):
|
for col, w in zip("ABCDE", (18, 16, 18, 50, 22)):
|
||||||
ws.column_dimensions[col].width = w
|
ws.column_dimensions[col].width = w
|
||||||
ws.freeze_panes = "A2"
|
ws.freeze_panes = "A2"
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ nothing is written.
|
||||||
import argparse, os, re, shutil, subprocess, sys, tempfile
|
import argparse, os, re, shutil, subprocess, sys, tempfile
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from common import (class_folder, load_taxonomy, mpn_make_prefix,
|
from common import (class_folder, load_taxonomy, mpn_make_prefix,
|
||||||
SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX)
|
SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX,
|
||||||
|
CHANGELOG_HEADERS, resolve_operator)
|
||||||
|
|
||||||
CFG_DEFAULT = os.path.join(os.path.dirname(__file__), "..", "config", "gitea.env")
|
CFG_DEFAULT = os.path.join(os.path.dirname(__file__), "..", "config", "gitea.env")
|
||||||
|
|
||||||
|
|
@ -89,9 +90,15 @@ def clone(env, dest, repo=None):
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
|
||||||
def commit_push(root, token, msg):
|
def commit_push(root, token, msg, author=None):
|
||||||
for args in (["config", "user.email", "datasheet-bot@local"],
|
"""Commit + push. `author` = (name, email) of the operator; when given, the commit is
|
||||||
["config", "user.name", "library-manager"],
|
authored by that real person (visible in git log / Gitea commit view / blame). Without it,
|
||||||
|
a generic bot identity is used."""
|
||||||
|
name, email = author or (None, None)
|
||||||
|
name = name or "library-manager"
|
||||||
|
email = email or "datasheet-bot@local"
|
||||||
|
for args in (["config", "user.email", email],
|
||||||
|
["config", "user.name", name],
|
||||||
["checkout", "-B", "main"], ["add", "-A"]):
|
["checkout", "-B", "main"], ["add", "-A"]):
|
||||||
subprocess.run(["git", "-C", root] + args, capture_output=True)
|
subprocess.run(["git", "-C", root] + args, capture_output=True)
|
||||||
if subprocess.run(["git", "-C", root, "diff", "--cached", "--quiet"]).returncode == 0:
|
if subprocess.run(["git", "-C", root, "diff", "--cached", "--quiet"]).returncode == 0:
|
||||||
|
|
@ -193,30 +200,50 @@ def cmd_place_part(env, args):
|
||||||
print(f"placed {tag} -> {cls}/{tag}")
|
print(f"placed {tag} -> {cls}/{tag}")
|
||||||
|
|
||||||
|
|
||||||
|
def _operator(args):
|
||||||
|
"""(name, email) of who's running this — from --author, else env, else identity file."""
|
||||||
|
return resolve_operator(getattr(args, "author", None))
|
||||||
|
|
||||||
|
|
||||||
|
def _by(name):
|
||||||
|
return f" (by {name})" if name else ""
|
||||||
|
|
||||||
|
|
||||||
def cmd_commit_push(env, args):
|
def cmd_commit_push(env, args):
|
||||||
commit_push(args.root, env.get("GIT_TOKEN", ""), args.message or "library-manager: update components")
|
name, email = _operator(args)
|
||||||
|
commit_push(args.root, env.get("GIT_TOKEN", ""),
|
||||||
|
args.message or f"library-manager: update components{_by(name)}", author=(name, email))
|
||||||
|
|
||||||
|
|
||||||
def cmd_push_part(env, args):
|
def cmd_push_part(env, args):
|
||||||
|
name, email = _operator(args)
|
||||||
tmp = tempfile.mkdtemp(prefix="components_")
|
tmp = tempfile.mkdtemp(prefix="components_")
|
||||||
clone(env, tmp)
|
clone(env, tmp)
|
||||||
cls, tag, _ = place_part(tmp, args.folder, args.typeid)
|
cls, tag, _ = place_part(tmp, args.folder, args.typeid)
|
||||||
print(f"placed {tag} -> {cls}/{tag}")
|
print(f"placed {tag} -> {cls}/{tag}" + (f" (by {name})" if name else ""))
|
||||||
commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or f"library-manager: add {tag}")
|
commit_push(tmp, env.get("GIT_TOKEN", ""),
|
||||||
|
args.message or f"library-manager: add {tag}{_by(name)}", author=(name, email))
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ skill repo + changelog
|
# ------------------------------------------------------------------ skill repo + changelog
|
||||||
|
|
||||||
|
|
||||||
def _changelog_rows(path):
|
def _changelog_rows(path):
|
||||||
"""(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent."""
|
"""(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent. Rows are
|
||||||
|
padded/truncated to the canonical column count so old 5-column changelogs (no 'By') merge
|
||||||
|
cleanly with new 6-column ones."""
|
||||||
import openpyxl
|
import openpyxl
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None, None, []
|
return None, None, []
|
||||||
|
n = len(CHANGELOG_HEADERS)
|
||||||
wb = openpyxl.load_workbook(path)
|
wb = openpyxl.load_workbook(path)
|
||||||
ws = wb["Changelog"] if "Changelog" in wb.sheetnames else wb.active
|
ws = wb["Changelog"] if "Changelog" in wb.sheetnames else wb.active
|
||||||
rows = [tuple("" if c is None else str(c) for c in r)
|
rows = []
|
||||||
for r in ws.iter_rows(min_row=2, values_only=True)]
|
for r in ws.iter_rows(min_row=2, values_only=True):
|
||||||
|
vals = ["" if c is None else str(c) for c in r]
|
||||||
|
vals = (vals + [""] * n)[:n]
|
||||||
|
if any(v for v in vals):
|
||||||
|
rows.append(tuple(vals))
|
||||||
return wb, ws, rows
|
return wb, ws, rows
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -234,6 +261,7 @@ def merge_changelog(local_path, repo_path):
|
||||||
shutil.copy(local_path, repo_path)
|
shutil.copy(local_path, repo_path)
|
||||||
_, _, rows = _changelog_rows(repo_path)
|
_, _, rows = _changelog_rows(repo_path)
|
||||||
return len(rows)
|
return len(rows)
|
||||||
|
from openpyxl.styles import Font, PatternFill
|
||||||
repo_wb, repo_ws, repo_rows = _changelog_rows(repo_path)
|
repo_wb, repo_ws, repo_rows = _changelog_rows(repo_path)
|
||||||
_, _, local_rows = _changelog_rows(local_path)
|
_, _, local_rows = _changelog_rows(local_path)
|
||||||
seen = set(repo_rows)
|
seen = set(repo_rows)
|
||||||
|
|
@ -245,16 +273,26 @@ def merge_changelog(local_path, repo_path):
|
||||||
seen.add(row)
|
seen.add(row)
|
||||||
added += 1
|
added += 1
|
||||||
merged.sort(key=lambda r: r[0]) # chronological by Date (column A)
|
merged.sort(key=lambda r: r[0]) # chronological by Date (column A)
|
||||||
if repo_ws.max_row > 1: # rewrite data rows, keep the header
|
n = len(CHANGELOG_HEADERS)
|
||||||
repo_ws.delete_rows(2, repo_ws.max_row - 1)
|
desc_col = CHANGELOG_HEADERS.index("Description") + 1
|
||||||
s = Side(style="thin", color="BFBFBF")
|
s = Side(style="thin", color="BFBFBF")
|
||||||
border = Border(left=s, right=s, top=s, bottom=s)
|
border = Border(left=s, right=s, top=s, bottom=s)
|
||||||
|
if repo_ws.max_row > 1: # clear everything, rewrite header + rows
|
||||||
|
repo_ws.delete_rows(1, repo_ws.max_row)
|
||||||
|
for c, h in enumerate(CHANGELOG_HEADERS, start=1): # canonical header (migrates 5->6 cols)
|
||||||
|
cell = repo_ws.cell(1, c, h)
|
||||||
|
cell.font = Font(name="Calibri", bold=True)
|
||||||
|
cell.fill = PatternFill("solid", fgColor="B6D7A8")
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
for row in merged:
|
for row in merged:
|
||||||
repo_ws.append(list(row))
|
rr = repo_ws.max_row + 1
|
||||||
rr = repo_ws.max_row
|
for c in range(1, n + 1):
|
||||||
for c in range(1, 6):
|
cell = repo_ws.cell(rr, c, row[c - 1] if c - 1 < len(row) else "")
|
||||||
repo_ws.cell(rr, c).border = border
|
cell.border = border
|
||||||
repo_ws.cell(rr, c).alignment = Alignment(vertical="center", wrap_text=(c == 5), horizontal="left")
|
cell.alignment = Alignment(vertical="center", wrap_text=(c == desc_col), horizontal="left")
|
||||||
|
repo_wb.freeze_panes = None
|
||||||
|
repo_ws.freeze_panes = "A2"
|
||||||
repo_wb.save(repo_path)
|
repo_wb.save(repo_path)
|
||||||
return added
|
return added
|
||||||
|
|
||||||
|
|
@ -297,7 +335,10 @@ def cmd_push_skill(env, args):
|
||||||
if os.path.exists(repo_cl): # sync merged history back to local
|
if os.path.exists(repo_cl): # sync merged history back to local
|
||||||
shutil.copy(repo_cl, local_cl)
|
shutil.copy(repo_cl, local_cl)
|
||||||
print(f"changelog: merged (+{added} new row(s)) into the skill repo's CHANGELOG.xlsx")
|
print(f"changelog: merged (+{added} new row(s)) into the skill repo's CHANGELOG.xlsx")
|
||||||
commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or "library-manager: sync skill files + changelog")
|
name, email = _operator(args)
|
||||||
|
commit_push(tmp, env.get("GIT_TOKEN", ""),
|
||||||
|
args.message or f"library-manager: sync skill files + changelog{_by(name)}",
|
||||||
|
author=(name, email))
|
||||||
|
|
||||||
|
|
||||||
def cmd_pull_skill(env, args):
|
def cmd_pull_skill(env, args):
|
||||||
|
|
@ -338,12 +379,14 @@ def main():
|
||||||
p.add_argument("--typeid", required=True); p.add_argument("--root"); p.add_argument("--local")
|
p.add_argument("--typeid", required=True); p.add_argument("--root"); p.add_argument("--local")
|
||||||
|
|
||||||
p = sub.add_parser("commit-push"); p.add_argument("--root", required=True)
|
p = sub.add_parser("commit-push"); p.add_argument("--root", required=True)
|
||||||
p.add_argument("--message")
|
p.add_argument("--message"); p.add_argument("--author", help="'Name <email>' of the operator")
|
||||||
|
|
||||||
p = sub.add_parser("push-part"); p.add_argument("--folder", required=True)
|
p = sub.add_parser("push-part"); p.add_argument("--folder", required=True)
|
||||||
p.add_argument("--typeid", required=True); p.add_argument("--message")
|
p.add_argument("--typeid", required=True); p.add_argument("--message")
|
||||||
|
p.add_argument("--author", help="'Name <email>' of the operator (who added the part)")
|
||||||
|
|
||||||
p = sub.add_parser("push-skill")
|
p = sub.add_parser("push-skill")
|
||||||
|
p.add_argument("--author", help="'Name <email>' of the operator (who made the change)")
|
||||||
p.add_argument("--src", default=os.path.join(os.path.dirname(__file__), ".."),
|
p.add_argument("--src", default=os.path.join(os.path.dirname(__file__), ".."),
|
||||||
help="skill directory to push (defaults to this skill)")
|
help="skill directory to push (defaults to this skill)")
|
||||||
p.add_argument("--message")
|
p.add_argument("--message")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue