@mmerterden/multi-agent-pipeline 20.8.1 → 20.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +75 -78
- package/package.json +4 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -14
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -31
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +11 -11
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
|
@@ -4,17 +4,23 @@ Every script that scans keys, names output files or orders languages imports
|
|
|
4
4
|
this module, so the scanner, the gate, the CMS sheet and the overlay agree on
|
|
5
5
|
what a wired key is, which rows are component-owned or dynamic, which files are
|
|
6
6
|
source, how a screen name becomes a file slug and which order languages take.
|
|
7
|
+
The key accessor type (`--key-type`, default `L10n`) and the language list
|
|
8
|
+
(`--langs`, default the languages present in the data) are inputs, never a
|
|
9
|
+
built-in list of one app's.
|
|
7
10
|
"""
|
|
11
|
+
import argparse
|
|
8
12
|
import os
|
|
9
13
|
import re
|
|
10
14
|
|
|
11
15
|
EXTENSIONS = {".swift", ".kt", ".kts", ".xml", ".ts", ".tsx", ".js", ".jsx", ".vue"}
|
|
12
16
|
SKIP_DIRS = {".git", "Generated"}
|
|
13
|
-
|
|
17
|
+
FALLBACK_LANG = "en"
|
|
18
|
+
RTL_LANGS = {"ar", "fa", "he", "ur"}
|
|
14
19
|
|
|
15
20
|
WORD = "A-Za-z0-9_"
|
|
16
21
|
IDENT = rf"[A-Za-z_][{WORD}]*"
|
|
17
|
-
|
|
22
|
+
DEFAULT_KEY_TYPES = ("L10n",)
|
|
23
|
+
KEY_TYPE_NAME = re.compile(rf"^{IDENT}$")
|
|
18
24
|
LITERAL_KEY = re.compile(rf"\"([{WORD}.]+)\"\.localized\b")
|
|
19
25
|
WEB_T = re.compile(r"(?<![\w$])t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
20
26
|
WEB_DOLLAR_T = re.compile(r"\$t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
@@ -28,6 +34,13 @@ RUNTIME_NOTE = "enumerate at runtime"
|
|
|
28
34
|
|
|
29
35
|
XML_INVALID = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f]")
|
|
30
36
|
|
|
37
|
+
ACCESSOR_EXTENSIONS = {".swift", ".kt", ".kts"}
|
|
38
|
+
COMMON_ACCESSORS = (
|
|
39
|
+
"L10n", "LocalizationKey", "LocalizationKeys",
|
|
40
|
+
"LocalizedStringKey", "LocalizedStrings", "Localizable", "Localization", "Strings", "Loc",
|
|
41
|
+
)
|
|
42
|
+
ACCESSOR_CHAIN = re.compile(rf"(?<![{WORD}.])([A-Z][{WORD}]*)((?:\.{IDENT}){{2,}})")
|
|
43
|
+
|
|
31
44
|
|
|
32
45
|
def strip_accessor(chain):
|
|
33
46
|
segments = chain.strip(".").split(".")
|
|
@@ -36,13 +49,101 @@ def strip_accessor(chain):
|
|
|
36
49
|
return ".".join(segments)
|
|
37
50
|
|
|
38
51
|
|
|
39
|
-
def
|
|
40
|
-
|
|
52
|
+
def key_types(values, fallback=DEFAULT_KEY_TYPES):
|
|
53
|
+
"""The accessor type names from repeated or comma-separated --key-type values.
|
|
54
|
+
|
|
55
|
+
Raises ValueError on a name that is not an identifier.
|
|
56
|
+
"""
|
|
57
|
+
names = [part.strip() for value in values or [] for part in str(value).split(",") if part.strip()]
|
|
58
|
+
for name in names:
|
|
59
|
+
if not KEY_TYPE_NAME.match(name):
|
|
60
|
+
raise ValueError(f"--key-type expects an identifier, got {name!r}")
|
|
61
|
+
return tuple(dict.fromkeys(names)) or tuple(fallback)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def key_type_alternation(types):
|
|
65
|
+
return "|".join(re.escape(name) for name in sorted(types, key=len, reverse=True))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def key_chain_pattern(types):
|
|
69
|
+
return re.compile(rf"(?:{key_type_alternation(types)})((?:\.{IDENT})+)")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lsk_keys(text, types=DEFAULT_KEY_TYPES):
|
|
73
|
+
for hit in key_chain_pattern(types).finditer(text):
|
|
41
74
|
key = strip_accessor(hit.group(1))
|
|
42
75
|
if key.count(".") >= 1:
|
|
43
76
|
yield key
|
|
44
77
|
|
|
45
78
|
|
|
79
|
+
def accessor_roots(texts, types=DEFAULT_KEY_TYPES):
|
|
80
|
+
"""Roots of key-like member chains that are not one of `types`, with their counts.
|
|
81
|
+
|
|
82
|
+
A chain counts when its root is a common accessor name or when it ends in
|
|
83
|
+
`.localized`, and it keeps two or more segments once that suffix is dropped.
|
|
84
|
+
"""
|
|
85
|
+
counts = {}
|
|
86
|
+
for text in texts:
|
|
87
|
+
for hit in ACCESSOR_CHAIN.finditer(text):
|
|
88
|
+
root, members = hit.group(1), hit.group(2)
|
|
89
|
+
if root in types:
|
|
90
|
+
continue
|
|
91
|
+
segments = members.strip(".").split(".")
|
|
92
|
+
localized = segments[-1].startswith("localized")
|
|
93
|
+
if strip_accessor(members).count(".") < 1:
|
|
94
|
+
continue
|
|
95
|
+
if root in COMMON_ACCESSORS or localized:
|
|
96
|
+
counts[root] = counts.get(root, 0) + 1
|
|
97
|
+
return dict(sorted(counts.items(), key=lambda item: (-item[1], item[0])))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def key_type_warning(texts, types=DEFAULT_KEY_TYPES, mapping_hint=False):
|
|
101
|
+
"""A warning when no chain of `types` is found but chains of another accessor are.
|
|
102
|
+
|
|
103
|
+
`texts` are the Swift and Kotlin sources read. Returns None when the
|
|
104
|
+
configured type matched at least once or nothing resembles an accessor.
|
|
105
|
+
"""
|
|
106
|
+
texts = list(texts)
|
|
107
|
+
if any(True for text in texts for _ in lsk_keys(text, types)):
|
|
108
|
+
return None
|
|
109
|
+
roots = accessor_roots(texts, types)
|
|
110
|
+
if not roots:
|
|
111
|
+
return None
|
|
112
|
+
seen = ", ".join(f"{root} ({count})" for root, count in list(roots.items())[:5])
|
|
113
|
+
where = "--key-type <Name> (or \"keyType\" in the mapping)" if mapping_hint else "--key-type <Name>"
|
|
114
|
+
return (
|
|
115
|
+
f"WARNING: no key chain of {', '.join(types)} was found, but the code has accessor chains rooted at "
|
|
116
|
+
f"{seen}. Set {where} to the project's key accessor type; until then its keys are "
|
|
117
|
+
f"missed or counted as weak."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def order_langs(langs):
|
|
122
|
+
"""en first when present, then the rest sorted; en alone when nothing is given."""
|
|
123
|
+
unique = list(dict.fromkeys(lang for lang in langs if lang))
|
|
124
|
+
rest = sorted(lang for lang in unique if lang != FALLBACK_LANG)
|
|
125
|
+
ordered = ([FALLBACK_LANG] if FALLBACK_LANG in unique else []) + rest
|
|
126
|
+
return ordered or [FALLBACK_LANG]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_langs(value, present=()):
|
|
130
|
+
"""A comma list in the order given, or for 'all' every language in `present`."""
|
|
131
|
+
text = str(value or "all").strip()
|
|
132
|
+
if text == "all":
|
|
133
|
+
return order_langs(present)
|
|
134
|
+
return [part.strip() for part in text.split(",") if part.strip()]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def reject_token_flag(cli, opts, env_hint):
|
|
138
|
+
"""Exit 2 when a credential was passed on argv, which other processes can read."""
|
|
139
|
+
if getattr(opts, "token", None) is not None:
|
|
140
|
+
cli.error(f"--token is not accepted, because other processes can read argv; {env_hint}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def add_token_flag(cli):
|
|
144
|
+
cli.add_argument("--token", help=argparse.SUPPRESS)
|
|
145
|
+
|
|
146
|
+
|
|
46
147
|
def is_interpolated(literal):
|
|
47
148
|
return bool(INTERPOLATION.search(literal))
|
|
48
149
|
|
|
@@ -23,7 +23,7 @@ import zipfile
|
|
|
23
23
|
from collections import namedtuple
|
|
24
24
|
from xml.sax.saxutils import escape, quoteattr
|
|
25
25
|
|
|
26
|
-
from _shared import
|
|
26
|
+
from _shared import RTL_LANGS, order_langs, parse_langs, slug, xml_text
|
|
27
27
|
|
|
28
28
|
WARN = "\u26a0\ufe0f"
|
|
29
29
|
DASH = "-"
|
|
@@ -210,14 +210,23 @@ def storage_image(filename, dimension, pixels):
|
|
|
210
210
|
return node("ac:image", [(f"ac:{dimension}", pixels)], target)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def mapping_langs(rows):
|
|
214
|
+
found = []
|
|
215
|
+
for row in rows:
|
|
216
|
+
for side in ("new", "legacy", "cms"):
|
|
217
|
+
found.extend((row.get(side) or {}).keys())
|
|
218
|
+
return order_langs(found)
|
|
219
|
+
|
|
220
|
+
|
|
213
221
|
class Doc:
|
|
214
|
-
def __init__(self, mapping, t, prefix, shots, out_dir):
|
|
222
|
+
def __init__(self, mapping, t, prefix, shots, out_dir, langs=None):
|
|
215
223
|
self.m = mapping
|
|
216
224
|
self.t = t
|
|
217
225
|
self.prefix = prefix
|
|
218
226
|
self.shots = shots
|
|
219
227
|
self.out = out_dir
|
|
220
228
|
self.rows = mapping.get("rows") or []
|
|
229
|
+
self.langs = list(langs) if langs else mapping_langs(self.rows)
|
|
221
230
|
self.counts = {v: sum(1 for r in self.rows if r.get("verdict") == v) for v in VERDICTS}
|
|
222
231
|
self.cms_rows = sum(1 for r in self.rows if (r.get("cms") or {}).get("tr") or (r.get("cms") or {}).get("en"))
|
|
223
232
|
self.drift_rows = sum(1 for r in self.rows if row_drifts(r))
|
|
@@ -246,7 +255,7 @@ class Doc:
|
|
|
246
255
|
]
|
|
247
256
|
|
|
248
257
|
def detail_langs(self, row):
|
|
249
|
-
return
|
|
258
|
+
return self.langs + [lang for lang in (row.get("cms") or {}) if lang not in self.langs]
|
|
250
259
|
|
|
251
260
|
def md(self):
|
|
252
261
|
t, m = self.t, self.m
|
|
@@ -572,7 +581,7 @@ def detail_blocks(doc, t):
|
|
|
572
581
|
blocks.append(para(f"{t['key']}: {row.get('newKey') or ''}"))
|
|
573
582
|
lines = [table_row(tcell(c, fill="F5F6F8") for c in t["detail_columns"])]
|
|
574
583
|
for lang in doc.detail_langs(row):
|
|
575
|
-
lines.append(table_row(tcell(v, rtl=lang
|
|
584
|
+
lines.append(table_row(tcell(v, rtl=lang in RTL_LANGS) for v in (lang, *lang_values(row, lang))))
|
|
576
585
|
blocks.append(table(lines))
|
|
577
586
|
return blocks
|
|
578
587
|
|
|
@@ -785,6 +794,11 @@ def main():
|
|
|
785
794
|
cli.add_argument("--pdf", action="store_true", help="add a PDF to the outputs")
|
|
786
795
|
cli.add_argument("--all", action="store_true", help="shorthand for --docx plus --pdf")
|
|
787
796
|
cli.add_argument("--print-upload", action="store_true", help="show the publish command and a hand-rolled curl version of it")
|
|
797
|
+
cli.add_argument(
|
|
798
|
+
"--langs", default="all",
|
|
799
|
+
help="languages of the details tables, comma list in display order; 'all' (default) takes every language "
|
|
800
|
+
"in the mapping, en first then the rest sorted",
|
|
801
|
+
)
|
|
788
802
|
opts = cli.parse_args()
|
|
789
803
|
|
|
790
804
|
mapping, mapping_dir = read_mapping(opts.mapping)
|
|
@@ -795,7 +809,8 @@ def main():
|
|
|
795
809
|
out_dir = os.path.abspath(opts.target_dir)
|
|
796
810
|
os.makedirs(out_dir, exist_ok=True)
|
|
797
811
|
shots = load_keyshots(mapping, mapping_dir, out_dir)
|
|
798
|
-
|
|
812
|
+
langs = None if opts.langs.strip() == "all" else parse_langs(opts.langs)
|
|
813
|
+
doc = Doc(mapping, T[opts.ui_lang], mapping.get("legacyKeyPrefix") or "", shots, out_dir, langs)
|
|
799
814
|
doc.base = mapping_dir
|
|
800
815
|
|
|
801
816
|
renderers = [("md", doc.md), ("confluence.xml", doc.xml), ("preview.html", doc.preview)]
|
|
@@ -17,6 +17,8 @@ import urllib.parse
|
|
|
17
17
|
import urllib.request
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
19
|
|
|
20
|
+
from _shared import add_token_flag, reject_token_flag
|
|
21
|
+
|
|
20
22
|
API = "https://api.figma.com/v1"
|
|
21
23
|
BATCH = 4
|
|
22
24
|
SEP = "[:\\-\u2013]"
|
|
@@ -117,9 +119,7 @@ def keychain_secret(service):
|
|
|
117
119
|
return secret if lookup.returncode == 0 and secret else None
|
|
118
120
|
|
|
119
121
|
|
|
120
|
-
def figma_token(
|
|
121
|
-
if explicit:
|
|
122
|
-
return explicit
|
|
122
|
+
def figma_token():
|
|
123
123
|
from_env = next((os.environ[v] for v in ("FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN") if os.environ.get(v)), None)
|
|
124
124
|
return from_env or keychain_secret("FIGMA_ACCESS_TOKEN")
|
|
125
125
|
|
|
@@ -210,11 +210,11 @@ def pick_source(opts, cli, file_key, nodes):
|
|
|
210
210
|
if opts.source == "rest":
|
|
211
211
|
if not have_target:
|
|
212
212
|
cli.error("--source rest needs a file key and node-ids (--file/--nodes or --mapping)")
|
|
213
|
-
token = figma_token(
|
|
213
|
+
token = figma_token()
|
|
214
214
|
if not token:
|
|
215
|
-
sys.exit("No Figma token found. Export FIGMA_ACCESS_TOKEN or FIGMA_TOKEN, keep it in the keychain under FIGMA_ACCESS_TOKEN
|
|
215
|
+
sys.exit("No Figma token found. Export FIGMA_ACCESS_TOKEN or FIGMA_TOKEN, or keep it in the keychain under FIGMA_ACCESS_TOKEN.")
|
|
216
216
|
return ("rest",) + from_rest(file_key, nodes, token, opts.ca_file)
|
|
217
|
-
token = figma_token(
|
|
217
|
+
token = figma_token() if have_target else None
|
|
218
218
|
if have_target and token:
|
|
219
219
|
try:
|
|
220
220
|
return ("rest",) + from_rest(file_key, nodes, token, opts.ca_file)
|
|
@@ -236,10 +236,11 @@ def main():
|
|
|
236
236
|
cli.add_argument("--from-mcp", help="JSON dump of annotations taken through MCP")
|
|
237
237
|
cli.add_argument("--local", help="screen folder of a design export that contains tree.json")
|
|
238
238
|
cli.add_argument("--source", default="auto", choices=["auto", "rest", "mcp", "local"])
|
|
239
|
-
cli
|
|
239
|
+
add_token_flag(cli)
|
|
240
240
|
cli.add_argument("--ca-file", help="PEM roots to trust when a proxy intercepts TLS")
|
|
241
241
|
cli.add_argument("--out", help="file to write; stdout when omitted")
|
|
242
242
|
opts = cli.parse_args()
|
|
243
|
+
reject_token_flag(cli, opts, "export FIGMA_ACCESS_TOKEN (or FIGMA_TOKEN), or keep it in the macOS keychain under FIGMA_ACCESS_TOKEN")
|
|
243
244
|
|
|
244
245
|
file_key = opts.file
|
|
245
246
|
nodes = [piece for piece in (opts.nodes or "").split(",") if piece.strip()]
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py
CHANGED
|
@@ -11,14 +11,13 @@ import os
|
|
|
11
11
|
import sys
|
|
12
12
|
import urllib.request
|
|
13
13
|
|
|
14
|
-
from _shared import
|
|
14
|
+
from _shared import parse_langs
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return [part.strip() for part in value.split(",") if part.strip()]
|
|
17
|
+
def snapshot_langs(snapshot_dir):
|
|
18
|
+
if not os.path.isdir(snapshot_dir):
|
|
19
|
+
return []
|
|
20
|
+
return [name[:-5] for name in os.listdir(snapshot_dir) if name.endswith(".json")]
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
def parse_pairs(pairs, flag):
|
|
@@ -81,7 +80,7 @@ def main():
|
|
|
81
80
|
cli.add_argument("--endpoint", required=True, help="URL template containing {lang} and, if needed, {env}")
|
|
82
81
|
cli.add_argument("--header", action="append", help="additional request header as K=V; may be repeated")
|
|
83
82
|
cli.add_argument("--headers-file", help="request headers as a JSON object, so secrets stay out of argv")
|
|
84
|
-
cli.add_argument("--langs", default="all", help="languages separated by commas, or 'all'")
|
|
83
|
+
cli.add_argument("--langs", default="all", help="languages separated by commas, or 'all' for every language already in the snapshot (en when it is empty)")
|
|
85
84
|
cli.add_argument("--env", default="", help="text put in place of {env}")
|
|
86
85
|
cli.add_argument("--status-field", default="statusCode", help="name of the response field that holds a status code")
|
|
87
86
|
cli.add_argument("--fail-status", action="append", help="CODE=message pair that marks a failed response; may be repeated")
|
|
@@ -93,7 +92,7 @@ def main():
|
|
|
93
92
|
|
|
94
93
|
headers = build_headers(opts)
|
|
95
94
|
fail_status = parse_pairs(opts.fail_status, "--fail-status")
|
|
96
|
-
langs = parse_langs(opts.langs)
|
|
95
|
+
langs = parse_langs(opts.langs, snapshot_langs(opts.snapshot_dir))
|
|
97
96
|
os.makedirs(opts.snapshot_dir, exist_ok=True)
|
|
98
97
|
|
|
99
98
|
total = 0
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py
CHANGED
|
@@ -18,9 +18,14 @@ import tempfile
|
|
|
18
18
|
import urllib.parse
|
|
19
19
|
from xml.sax.saxutils import escape, quoteattr
|
|
20
20
|
|
|
21
|
+
from _shared import add_token_flag, reject_token_flag
|
|
22
|
+
|
|
21
23
|
EMBED_EXTENSIONS = {".xlsx", ".xls", ".csv", ".docx", ".pdf"}
|
|
22
24
|
EMBED_HEADING = "CMS Import File"
|
|
23
25
|
CONTENT = "/rest/api/content"
|
|
26
|
+
CONNECT_TIMEOUT = 15
|
|
27
|
+
REQUEST_TIMEOUT = 120
|
|
28
|
+
PROCESS_TIMEOUT = 150
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class Client:
|
|
@@ -39,7 +44,10 @@ class Client:
|
|
|
39
44
|
os.rmdir(self.tmp)
|
|
40
45
|
|
|
41
46
|
def call(self, method, path, body=None, form_file=None):
|
|
42
|
-
argv = [
|
|
47
|
+
argv = [
|
|
48
|
+
"curl", "-sS", "--connect-timeout", str(CONNECT_TIMEOUT), "--max-time", str(REQUEST_TIMEOUT),
|
|
49
|
+
"-K", self.config, "-X", method,
|
|
50
|
+
]
|
|
43
51
|
if body is not None:
|
|
44
52
|
payload = os.path.join(self.tmp, "body.json")
|
|
45
53
|
with open(payload, "w", encoding="utf-8") as sink:
|
|
@@ -48,7 +56,10 @@ class Client:
|
|
|
48
56
|
if form_file is not None:
|
|
49
57
|
argv += ["-F", "file=@" + form_file, "-H", "X-Atlassian-Token: nocheck"]
|
|
50
58
|
argv.append(self.base + path)
|
|
51
|
-
|
|
59
|
+
try:
|
|
60
|
+
done = subprocess.run(argv, text=True, capture_output=True, timeout=PROCESS_TIMEOUT)
|
|
61
|
+
except subprocess.TimeoutExpired:
|
|
62
|
+
sys.exit(f"curl did not finish within {PROCESS_TIMEOUT}s: {method} {path}")
|
|
52
63
|
if done.returncode != 0:
|
|
53
64
|
sys.exit(f"curl did not complete: {done.stderr.strip()}")
|
|
54
65
|
try:
|
|
@@ -60,9 +71,7 @@ class Client:
|
|
|
60
71
|
return reply
|
|
61
72
|
|
|
62
73
|
|
|
63
|
-
def resolve_token(
|
|
64
|
-
if explicit:
|
|
65
|
-
return explicit
|
|
74
|
+
def resolve_token(keychain_name):
|
|
66
75
|
if os.environ.get("CONFLUENCE_API_TOKEN"):
|
|
67
76
|
return os.environ["CONFLUENCE_API_TOKEN"]
|
|
68
77
|
try:
|
|
@@ -111,7 +120,7 @@ def build_cli():
|
|
|
111
120
|
cli.add_argument("--base", "--base-url", dest="base", default=os.environ.get("CONFLUENCE_BASE_URL"))
|
|
112
121
|
cli.add_argument("--space", default=os.environ.get("CONFLUENCE_SPACE"))
|
|
113
122
|
cli.add_argument("--parent", default=os.environ.get("CONFLUENCE_PARENT"), help="id of the parent page")
|
|
114
|
-
cli
|
|
123
|
+
add_token_flag(cli)
|
|
115
124
|
cli.add_argument("--keychain-name", default="CONFLUENCE_API_TOKEN", help="keychain service that stores the PAT")
|
|
116
125
|
cli.add_argument("--embed-heading", default=EMBED_HEADING, help="heading above the embedded spreadsheet / document previews")
|
|
117
126
|
cli.add_argument("--dry-run", action="store_true", help="report the planned actions without touching Confluence")
|
|
@@ -163,6 +172,10 @@ def upload(client, page_id, path):
|
|
|
163
172
|
def main():
|
|
164
173
|
cli = build_cli()
|
|
165
174
|
opts = cli.parse_args()
|
|
175
|
+
reject_token_flag(
|
|
176
|
+
cli, opts,
|
|
177
|
+
f"export CONFLUENCE_API_TOKEN, or keep it in the macOS keychain under the --keychain-name service ({opts.keychain_name})",
|
|
178
|
+
)
|
|
166
179
|
check_inputs(cli, opts)
|
|
167
180
|
title = opts.title or f"{opts.title_prefix} - {opts.screen}"
|
|
168
181
|
|
|
@@ -185,7 +198,7 @@ def main():
|
|
|
185
198
|
print(json.dumps(plan, indent=2, ensure_ascii=False))
|
|
186
199
|
return
|
|
187
200
|
|
|
188
|
-
token = resolve_token(opts.
|
|
201
|
+
token = resolve_token(opts.keychain_name)
|
|
189
202
|
if not token:
|
|
190
203
|
sys.exit(
|
|
191
204
|
"No Confluence token available. Put a Server/DC personal access token in the keychain "
|
|
@@ -145,11 +145,12 @@ def main():
|
|
|
145
145
|
cli.add_argument("--nodes", help="frame node-ids separated by commas; falls back to figmaNodes")
|
|
146
146
|
cli.add_argument("--pad", default=150, type=int, help="pixels of surrounding context kept over and under the element")
|
|
147
147
|
cli.add_argument("--scale", default=2, type=int)
|
|
148
|
-
|
|
148
|
+
ov.add_token_flag(cli)
|
|
149
149
|
cli.add_argument("--ca-file", help="PEM roots to trust when a proxy intercepts TLS")
|
|
150
150
|
cli.add_argument("--out", dest="target_dir", default=".", help="folder for the manifest; the crops go into <out>/keyshots/")
|
|
151
151
|
cli.add_argument("--slug", help="stem of the manifest name; derived from the screen name when omitted")
|
|
152
152
|
opts = cli.parse_args()
|
|
153
|
+
ov.reject_token_flag(cli, opts, ov.FIGMA_TOKEN_HINT)
|
|
153
154
|
|
|
154
155
|
with open(opts.mapping, encoding="utf-8") as stream:
|
|
155
156
|
mapping = json.load(stream)
|
|
@@ -163,9 +164,9 @@ def main():
|
|
|
163
164
|
nodes = ov.split_nodes(opts.nodes, mapping)
|
|
164
165
|
if not (file_key and nodes):
|
|
165
166
|
cli.error("need a Figma file key and frame node-ids (flags or mapping), or --spec")
|
|
166
|
-
token = ov.figma_token(
|
|
167
|
+
token = ov.figma_token()
|
|
167
168
|
if not token:
|
|
168
|
-
sys.exit("No Figma token found (FIGMA_ACCESS_TOKEN, FIGMA_TOKEN
|
|
169
|
+
sys.exit("No Figma token found (FIGMA_ACCESS_TOKEN, FIGMA_TOKEN or keychain FIGMA_ACCESS_TOKEN). "
|
|
169
170
|
"Without one, build --spec files from the screenshots instead.")
|
|
170
171
|
pages = rest_pages(file_key, nodes, opts.scale, token, opts.ca_file)
|
|
171
172
|
|
|
@@ -25,7 +25,9 @@ import urllib.error
|
|
|
25
25
|
import urllib.parse
|
|
26
26
|
import urllib.request
|
|
27
27
|
|
|
28
|
-
from _shared import slug
|
|
28
|
+
from _shared import add_token_flag, reject_token_flag, slug
|
|
29
|
+
|
|
30
|
+
FIGMA_TOKEN_HINT = "export FIGMA_ACCESS_TOKEN (or FIGMA_TOKEN), or keep it in the macOS keychain under FIGMA_ACCESS_TOKEN"
|
|
29
31
|
|
|
30
32
|
API = "https://api.figma.com/v1"
|
|
31
33
|
SEP = "[:\\-\u2013]"
|
|
@@ -137,9 +139,7 @@ def keychain_secret(service):
|
|
|
137
139
|
return secret if lookup.returncode == 0 and secret else None
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
def figma_token(
|
|
141
|
-
if explicit:
|
|
142
|
-
return explicit
|
|
142
|
+
def figma_token():
|
|
143
143
|
from_env = next((os.environ[v] for v in ("FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN") if os.environ.get(v)), None)
|
|
144
144
|
return from_env or keychain_secret("FIGMA_ACCESS_TOKEN")
|
|
145
145
|
|
|
@@ -468,7 +468,7 @@ def build_cli():
|
|
|
468
468
|
cli.add_argument("--file", help="Figma file key; falls back to figmaFileKey in the mapping")
|
|
469
469
|
cli.add_argument("--nodes", help="node-ids separated by commas, the screen frame first; falls back to figmaNodes")
|
|
470
470
|
cli.add_argument("--scale", default=2, type=int)
|
|
471
|
-
cli
|
|
471
|
+
add_token_flag(cli)
|
|
472
472
|
cli.add_argument("--ca-file", help="PEM roots to trust when a proxy intercepts TLS")
|
|
473
473
|
cli.add_argument("--out", dest="target_dir", default=".", help="folder that receives the generated files")
|
|
474
474
|
cli.add_argument("--slug", help="stem for the file names; derived from the screen name when omitted")
|
|
@@ -479,6 +479,7 @@ def build_cli():
|
|
|
479
479
|
def main():
|
|
480
480
|
cli = build_cli()
|
|
481
481
|
opts = cli.parse_args()
|
|
482
|
+
reject_token_flag(cli, opts, FIGMA_TOKEN_HINT)
|
|
482
483
|
labels = LABELS[opts.ui_lang]
|
|
483
484
|
|
|
484
485
|
with open(opts.mapping, encoding="utf-8") as stream:
|
|
@@ -499,9 +500,9 @@ def main():
|
|
|
499
500
|
nodes = split_nodes(opts.nodes, mapping)
|
|
500
501
|
if not (file_key and nodes):
|
|
501
502
|
cli.error("need a Figma file key and frame node-id (flags or mapping), or --spec")
|
|
502
|
-
token = figma_token(
|
|
503
|
+
token = figma_token()
|
|
503
504
|
if not token:
|
|
504
|
-
sys.exit("No Figma token found (FIGMA_ACCESS_TOKEN, FIGMA_TOKEN
|
|
505
|
+
sys.exit("No Figma token found (FIGMA_ACCESS_TOKEN, FIGMA_TOKEN or keychain FIGMA_ACCESS_TOKEN). "
|
|
505
506
|
"Without one, measure the boxes on the screenshot and hand them over with --spec.")
|
|
506
507
|
page = page_from_rest(file_key, canon_id(nodes[0]), opts.scale, token, opts.ca_file)
|
|
507
508
|
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py
CHANGED
|
@@ -13,7 +13,7 @@ import plistlib
|
|
|
13
13
|
import sys
|
|
14
14
|
import urllib.request
|
|
15
15
|
|
|
16
|
-
from _shared import
|
|
16
|
+
from _shared import parse_langs
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
@@ -21,10 +21,17 @@ def split_csv(value):
|
|
|
21
21
|
return [part.strip() for part in value.split(",") if part.strip()]
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def present_langs(snapshot_root, plist_root):
|
|
25
|
+
found = []
|
|
26
|
+
if snapshot_root and os.path.isdir(snapshot_root):
|
|
27
|
+
found += [name[:-5] for name in os.listdir(snapshot_root) if name.endswith(".json")]
|
|
28
|
+
if plist_root and os.path.isdir(plist_root):
|
|
29
|
+
for name in os.listdir(plist_root):
|
|
30
|
+
if name.endswith(".lproj") and os.path.isfile(os.path.join(plist_root, name, "language.plist")):
|
|
31
|
+
found.append(name[: -len(".lproj")])
|
|
32
|
+
elif name.endswith(".plist") and name != "language.plist":
|
|
33
|
+
found.append(name[: -len(".plist")])
|
|
34
|
+
return found
|
|
28
35
|
|
|
29
36
|
|
|
30
37
|
def parse_pairs(pairs, flag):
|
|
@@ -134,7 +141,7 @@ def from_live(args, keys, langs):
|
|
|
134
141
|
def main():
|
|
135
142
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
136
143
|
parser.add_argument("--keys", required=True, help="legacy keys separated by commas; leave off the backend prefix")
|
|
137
|
-
parser.add_argument("--langs", default="
|
|
144
|
+
parser.add_argument("--langs", default="all", help="comma list, or 'all' for every language the snapshot or plist root holds")
|
|
138
145
|
parser.add_argument("--snapshot-root", help="directory holding <lang>.json flat snapshots")
|
|
139
146
|
parser.add_argument("--live", action="store_true", help="query the label endpoint instead of the snapshot")
|
|
140
147
|
parser.add_argument("--endpoint", help="URL template with {lang} and optional {env}")
|
|
@@ -145,11 +152,11 @@ def main():
|
|
|
145
152
|
parser.add_argument("--fail-status", action="append", help="CODE=message treated as a failure (repeatable)")
|
|
146
153
|
parser.add_argument("--timeout", type=int, default=30)
|
|
147
154
|
parser.add_argument("--plist-root", help="last resort: directory of bundled <lang>.lproj/language.plist")
|
|
148
|
-
parser.add_argument("--prefix", default="", help="backend key prefix the app strips, e.g.
|
|
155
|
+
parser.add_argument("--prefix", default="", help="backend key prefix the app strips, e.g. App-")
|
|
149
156
|
args = parser.parse_args()
|
|
150
157
|
|
|
151
158
|
keys = split_csv(args.keys)
|
|
152
|
-
langs = parse_langs(args.langs)
|
|
159
|
+
langs = parse_langs(args.langs, present_langs(args.snapshot_root, args.plist_root))
|
|
153
160
|
if not (args.snapshot_root or args.plist_root or args.endpoint):
|
|
154
161
|
sys.exit("no source given: pass --snapshot-root, --plist-root, or --live with --endpoint")
|
|
155
162
|
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py
CHANGED
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
import sys
|
|
13
13
|
|
|
14
|
-
from _shared import
|
|
14
|
+
from _shared import normalize_key, parse_langs
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
DOMAIN_DIRS = {
|
|
@@ -20,12 +20,6 @@ DOMAIN_DIRS = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def parse_langs(value):
|
|
24
|
-
if value.strip() == "all":
|
|
25
|
-
return list(ALL_LANGS)
|
|
26
|
-
return [part.strip() for part in value.split(",") if part.strip()]
|
|
27
|
-
|
|
28
|
-
|
|
29
23
|
def read_keys(opts):
|
|
30
24
|
raw = [piece.strip() for piece in opts.keys.split(",")] if opts.keys else []
|
|
31
25
|
if opts.keys_file:
|
|
@@ -81,6 +75,20 @@ def read_suggested(path, langs):
|
|
|
81
75
|
return {lang: value_of(lang) for lang in langs}
|
|
82
76
|
|
|
83
77
|
|
|
78
|
+
def present_langs(directory, catalog, keys):
|
|
79
|
+
found = []
|
|
80
|
+
for key in keys:
|
|
81
|
+
path = os.path.join(directory, normalize_key(key) + ".json")
|
|
82
|
+
if os.path.isfile(path):
|
|
83
|
+
data = read_json(path)
|
|
84
|
+
found.extend(data.keys() if isinstance(data, dict) else [])
|
|
85
|
+
for candidate in dict.fromkeys((key, normalize_key(key))):
|
|
86
|
+
entry = (catalog or {}).get(candidate)
|
|
87
|
+
if isinstance(entry, dict):
|
|
88
|
+
found.extend(entry.keys())
|
|
89
|
+
return found
|
|
90
|
+
|
|
91
|
+
|
|
84
92
|
def from_catalog(catalog, key, langs):
|
|
85
93
|
for candidate in dict.fromkeys((key, normalize_key(key))):
|
|
86
94
|
entry = catalog.get(candidate)
|
|
@@ -101,7 +109,7 @@ def main():
|
|
|
101
109
|
cli.add_argument("--domain", default="localization", choices=sorted(DOMAIN_DIRS))
|
|
102
110
|
cli.add_argument("--catalog", help="string catalog the app ships (.xcstrings or flat JSON), consulted when a key has no Suggested file")
|
|
103
111
|
cli.add_argument("--report-source", action="store_true", help="return {source, values} per key instead of the bare values")
|
|
104
|
-
cli.add_argument("--langs", default="
|
|
112
|
+
cli.add_argument("--langs", default="all", help="languages separated by commas, or 'all' for every language the sources hold")
|
|
105
113
|
opts = cli.parse_args()
|
|
106
114
|
|
|
107
115
|
keys = read_keys(opts)
|
|
@@ -113,8 +121,8 @@ def main():
|
|
|
113
121
|
if not os.path.isdir(directory):
|
|
114
122
|
cli.error(f"no Suggested folder at {directory} (check --resources-root / --suggested-dir / --domain)")
|
|
115
123
|
|
|
116
|
-
langs = parse_langs(opts.langs)
|
|
117
124
|
catalog = load_catalog(opts.catalog) if opts.catalog else None
|
|
125
|
+
langs = parse_langs(opts.langs, present_langs(directory, catalog, keys) if opts.langs.strip() == "all" else ())
|
|
118
126
|
|
|
119
127
|
result, via_catalog, unresolved = {}, [], []
|
|
120
128
|
for key in keys:
|