@salesforce/afv-skills 1.53.0 → 1.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/dx-org-analyze/README.md +310 -0
- package/skills/dx-org-analyze/SKILL.md +261 -0
- package/skills/dx-org-analyze/references/collection-details.md +88 -0
- package/skills/dx-org-analyze/references/report-format.md +67 -0
- package/skills/dx-org-analyze/scripts/collect_org_data.py +823 -0
- package/skills/dx-org-analyze/scripts/compute_diff.py +1103 -0
- package/skills/dx-org-analyze/scripts/introspect_org.py +436 -0
- package/skills/dx-org-analyze/tests/README.md +46 -0
- package/skills/dx-org-analyze/tests/fixtures/org_a.json +76 -0
- package/skills/dx-org-analyze/tests/fixtures/org_b.json +70 -0
- package/skills/dx-org-analyze/tests/test_compute_diff.sh +284 -0
- package/skills/dx-org-analyze/tests/test_consistency.sh +428 -0
- package/skills/experience-design-validate/SKILL.md +163 -0
- package/skills/experience-design-validate/references/ai.md +54 -0
- package/skills/experience-design-validate/references/components.md +61 -0
- package/skills/experience-design-validate/references/craft.md +158 -0
- package/skills/experience-design-validate/references/data.md +59 -0
- package/skills/experience-design-validate/references/forms-flows.md +57 -0
- package/skills/experience-design-validate/references/interaction.md +53 -0
- package/skills/experience-design-validate/references/navigation.md +51 -0
- package/skills/experience-design-validate/references/performance.md +56 -0
- package/skills/experience-design-validate/references/records.md +60 -0
- package/skills/experience-design-validate/references/responsive.md +48 -0
- package/skills/experience-design-validate/references/scoring-rubric.md +257 -0
- package/skills/experience-design-validate/references/state.md +53 -0
- package/skills/experience-design-validate/references/trust.md +47 -0
- package/skills/experience-design-validate/references/usability.md +35 -0
- package/skills/experience-design-validate/references/visual-system.md +183 -0
- package/skills/service-agentforce-contact-center-coordinate/SKILL.md +170 -0
- package/skills/service-agentforce-contact-center-coordinate/assets/escalation-flow.flow-meta.xml +72 -0
- package/skills/service-agentforce-contact-center-coordinate/assets/omni-flow.flow-meta.xml +85 -0
- package/skills/service-agentforce-contact-center-coordinate/assets/report-template.md +52 -0
- package/skills/service-agentforce-contact-center-coordinate/references/agentforce-prerequisite.md +31 -0
- package/skills/service-agentforce-contact-center-coordinate/references/messaging_channel.md +76 -0
- package/skills/service-agentforce-contact-center-coordinate/references/number_management_api.md +78 -0
- package/skills/service-agentforce-contact-center-coordinate/references/omni-flow-routing.md +74 -0
- package/skills/service-agentforce-contact-center-coordinate/references/setup_summary.md +36 -0
- package/skills/service-agentforce-contact-center-coordinate/references/verification_and_errors.md +42 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/check-agentforce-prereq.sh +54 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/create-routing-flows.sh +49 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/create-voice-agent.sh +73 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/create-voice-channel.sh +66 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/fetch-numbers.sh +21 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/lib.sh +46 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/prepare-agent-workdir.sh +21 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/procure-number.sh +24 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-acc-queue.sh +27 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-channel-line.sh +37 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/resolve-flow-definition.sh +33 -0
- package/skills/service-agentforce-contact-center-coordinate/scripts/verify-number-live.sh +63 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a single-org introspection report.
|
|
3
|
+
|
|
4
|
+
Takes one collected org data directory (output of collect_org_data.py)
|
|
5
|
+
and produces a summary report in markdown and/or JSON.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
introspect_org.py --org /tmp/org-data --output /tmp/report
|
|
9
|
+
introspect_org.py --org /tmp/org-data --output /tmp/report --format markdown
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import date
|
|
17
|
+
|
|
18
|
+
# ── Reuse categorization from compute_diff ──────────────────────────
|
|
19
|
+
|
|
20
|
+
PERM_CATEGORIES = [
|
|
21
|
+
("Commerce", ["commerce", "b2b", "b2c", "order", "cart", "store", "checkout"]),
|
|
22
|
+
("Platform/API", ["platform", "api", "rest", "soap", "bulk", "streaming", "connect", "metadata"]),
|
|
23
|
+
("Security", ["security", "auth", "login", "mfa", "sso", "encryption", "certificate", "session", "password", "oauth"]),
|
|
24
|
+
("Einstein/AI", ["einstein", "ai", "predict", "ml", "copilot", "agent"]),
|
|
25
|
+
("Service", ["service", "case", "knowledge", "chat", "messaging", "omni", "entitlement"]),
|
|
26
|
+
("Data", ["data", "record", "report", "dashboard", "analytics", "tableau"]),
|
|
27
|
+
("Trialforce", ["trial", "trialforce", "tso", "tmo", "signup", "template", "edition"]),
|
|
28
|
+
("Sales", ["sales", "opportunity", "lead", "forecast", "quote", "contract"]),
|
|
29
|
+
("Work/HR", ["work", "hr", "employee", "shift", "wellness"]),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
VALUE_CATEGORIES = [
|
|
33
|
+
("Identity", ["name", "edition", "org", "domain", "id", "type", "instance", "locale"]),
|
|
34
|
+
("Storage", ["storage", "file", "size", "space", "quota"]),
|
|
35
|
+
("Feature Limits", ["max", "limit", "count", "num", "enable"]),
|
|
36
|
+
("API", ["api", "call", "request", "bandwidth"]),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def classify(name, categories, default="Other"):
|
|
41
|
+
lower = name.lower()
|
|
42
|
+
for cat, keywords in categories:
|
|
43
|
+
if any(kw in lower for kw in keywords):
|
|
44
|
+
return cat
|
|
45
|
+
return default
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Data loading (same as compute_diff.py) ──────────────────────────
|
|
49
|
+
|
|
50
|
+
def load_org_data(path):
|
|
51
|
+
if os.path.isdir(path):
|
|
52
|
+
combined = os.path.join(path, "combined.json")
|
|
53
|
+
if os.path.exists(combined):
|
|
54
|
+
with open(combined) as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
manifest_path = os.path.join(path, "manifest.json")
|
|
57
|
+
if not os.path.exists(manifest_path):
|
|
58
|
+
raise FileNotFoundError(f"Directory {path} has no manifest.json or combined.json")
|
|
59
|
+
with open(manifest_path) as f:
|
|
60
|
+
manifest = json.load(f)
|
|
61
|
+
data = {}
|
|
62
|
+
for cat, fname in manifest.get("files", {}).items():
|
|
63
|
+
fpath = os.path.join(path, fname)
|
|
64
|
+
with open(fpath) as f:
|
|
65
|
+
chunk = json.load(f)
|
|
66
|
+
if isinstance(chunk, dict):
|
|
67
|
+
data.update(chunk)
|
|
68
|
+
else:
|
|
69
|
+
data[cat] = chunk
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
with open(path) as f:
|
|
73
|
+
return json.load(f)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Analysis ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def analyze_metadata(data):
|
|
79
|
+
types = data.get("metadata_types", [])
|
|
80
|
+
components = data.get("metadata_components", {})
|
|
81
|
+
by_type = {}
|
|
82
|
+
total = 0
|
|
83
|
+
namespaced_total = 0
|
|
84
|
+
for mtype, items in sorted(components.items()):
|
|
85
|
+
org_native = [c for c in items if not c.get("namespace")]
|
|
86
|
+
namespaced = [c for c in items if c.get("namespace")]
|
|
87
|
+
org_native_names = sorted(
|
|
88
|
+
c.get("fullName") or c.get("name", "") for c in org_native
|
|
89
|
+
)
|
|
90
|
+
namespaced_by_ns = {}
|
|
91
|
+
for c in namespaced:
|
|
92
|
+
ns = c.get("namespace", "unknown")
|
|
93
|
+
namespaced_by_ns.setdefault(ns, []).append(
|
|
94
|
+
c.get("fullName") or c.get("name", "")
|
|
95
|
+
)
|
|
96
|
+
for ns in namespaced_by_ns:
|
|
97
|
+
namespaced_by_ns[ns] = sorted(namespaced_by_ns[ns])
|
|
98
|
+
by_type[mtype] = {
|
|
99
|
+
"total": len(items),
|
|
100
|
+
"org_native": len(org_native),
|
|
101
|
+
"namespaced": len(namespaced),
|
|
102
|
+
"org_native_names": org_native_names,
|
|
103
|
+
"namespaced_by_ns": namespaced_by_ns,
|
|
104
|
+
}
|
|
105
|
+
total += len(org_native)
|
|
106
|
+
namespaced_total += len(namespaced)
|
|
107
|
+
return {
|
|
108
|
+
"types_discovered": len(types),
|
|
109
|
+
"types_with_components": len(components),
|
|
110
|
+
"total_org_native": total,
|
|
111
|
+
"total_namespaced": namespaced_total,
|
|
112
|
+
"by_type": by_type,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def analyze_permissions(data):
|
|
117
|
+
perms = data.get("permissions", {})
|
|
118
|
+
by_category = {}
|
|
119
|
+
enabled = 0
|
|
120
|
+
disabled = 0
|
|
121
|
+
non_boolean = 0
|
|
122
|
+
for name, val in perms.items():
|
|
123
|
+
cat = classify(name, PERM_CATEGORIES)
|
|
124
|
+
if cat not in by_category:
|
|
125
|
+
by_category[cat] = {"enabled": 0, "disabled": 0, "non_boolean": 0, "items": []}
|
|
126
|
+
v = val if isinstance(val, str) else val.get("value", str(val)) if isinstance(val, dict) else str(val)
|
|
127
|
+
if v.lower() in ("enabled", "true"):
|
|
128
|
+
by_category[cat]["enabled"] += 1
|
|
129
|
+
enabled += 1
|
|
130
|
+
elif v.lower() in ("disabled", "false"):
|
|
131
|
+
by_category[cat]["disabled"] += 1
|
|
132
|
+
disabled += 1
|
|
133
|
+
else:
|
|
134
|
+
by_category[cat]["non_boolean"] += 1
|
|
135
|
+
non_boolean += 1
|
|
136
|
+
by_category[cat]["items"].append((name, v))
|
|
137
|
+
return {
|
|
138
|
+
"total": len(perms),
|
|
139
|
+
"enabled": enabled,
|
|
140
|
+
"disabled": disabled,
|
|
141
|
+
"non_boolean": non_boolean,
|
|
142
|
+
"by_category": by_category,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def analyze_org_values(data):
|
|
147
|
+
vals = data.get("org_values", {})
|
|
148
|
+
by_category = {}
|
|
149
|
+
for name, val in vals.items():
|
|
150
|
+
cat = classify(name, VALUE_CATEGORIES)
|
|
151
|
+
if cat not in by_category:
|
|
152
|
+
by_category[cat] = []
|
|
153
|
+
by_category[cat].append((name, val))
|
|
154
|
+
return {"total": len(vals), "by_category": by_category}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def analyze_packages(data):
|
|
158
|
+
pkgs = data.get("installed_packages", [])
|
|
159
|
+
return {
|
|
160
|
+
"count": len(pkgs),
|
|
161
|
+
"packages": [
|
|
162
|
+
{"name": p.get("name", ""), "namespace": p.get("namespace") or "", "version": p.get("version", "")}
|
|
163
|
+
for p in pkgs
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def analyze_licenses(data):
|
|
169
|
+
lics = data.get("licenses", {})
|
|
170
|
+
result = {}
|
|
171
|
+
for category in ("user", "permission_set", "package"):
|
|
172
|
+
items = lics.get(category, [])
|
|
173
|
+
result[category] = {
|
|
174
|
+
"count": len(items),
|
|
175
|
+
"items": items,
|
|
176
|
+
}
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def analyze_system_permissions(data):
|
|
181
|
+
sp_raw = data.get("system_permissions", {})
|
|
182
|
+
if isinstance(sp_raw, dict):
|
|
183
|
+
sp = sp_raw.get("permission_sets", [])
|
|
184
|
+
else:
|
|
185
|
+
sp = sp_raw
|
|
186
|
+
total_enabled = 0
|
|
187
|
+
sets = []
|
|
188
|
+
for ps in sp:
|
|
189
|
+
name = ps.get("name", ps.get("label", "Unknown"))
|
|
190
|
+
perms = ps.get("permissions", {})
|
|
191
|
+
enabled = sum(1 for v in perms.values() if v)
|
|
192
|
+
total_enabled += enabled
|
|
193
|
+
sets.append({"name": name, "enabled": enabled, "total": len(perms)})
|
|
194
|
+
return {"permission_sets": len(sp), "total_enabled": total_enabled, "sets": sets}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def analyze_deep_data(data):
|
|
198
|
+
dd = data.get("deep_data", {})
|
|
199
|
+
result = {}
|
|
200
|
+
for category, records in dd.items():
|
|
201
|
+
result[category] = len(records) if isinstance(records, list) else 0
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ── Render markdown ─────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def render_markdown(data, meta, perms, vals, pkgs, lics, sys_perms, deep, label):
|
|
208
|
+
lines = []
|
|
209
|
+
lines.append(f"# Org Analysis Report: {label}\n")
|
|
210
|
+
lines.append(f"| Field | Value |")
|
|
211
|
+
lines.append(f"|-------|-------|")
|
|
212
|
+
lines.append(f"| **Org ID** | {data.get('org_id', 'N/A')} |")
|
|
213
|
+
lines.append(f"| **Name** | {data.get('org_name', 'N/A')} |")
|
|
214
|
+
lines.append(f"| **Edition** | {data.get('org_edition', 'N/A')} |")
|
|
215
|
+
lines.append(f"| **Date** | {date.today()} |")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
# Summary
|
|
219
|
+
lines.append("---\n")
|
|
220
|
+
lines.append("## Summary\n")
|
|
221
|
+
lines.append("| Category | Count |")
|
|
222
|
+
lines.append("|----------|-------|")
|
|
223
|
+
lines.append(f"| Metadata types discovered | {meta['types_discovered']} |")
|
|
224
|
+
lines.append(f"| Types with components | {meta['types_with_components']} |")
|
|
225
|
+
lines.append(f"| Org-native components | {meta['total_org_native']} |")
|
|
226
|
+
lines.append(f"| Namespaced components | {meta['total_namespaced']} |")
|
|
227
|
+
lines.append(f"| Org settings | {perms['total']} ({perms['enabled']} enabled, {perms['disabled']} disabled) |")
|
|
228
|
+
lines.append(f"| Org limits | {vals['total']} |")
|
|
229
|
+
lines.append(f"| Installed packages | {pkgs['count']} |")
|
|
230
|
+
lines.append(f"| System permission sets | {sys_perms['permission_sets']} |")
|
|
231
|
+
deep_total = sum(deep.values())
|
|
232
|
+
lines.append(f"| Deep data records | {deep_total} |")
|
|
233
|
+
errors = data.get("errors", [])
|
|
234
|
+
if errors:
|
|
235
|
+
lines.append(f"| Collection warnings | {len(errors)} |")
|
|
236
|
+
lines.append("")
|
|
237
|
+
|
|
238
|
+
# Metadata inventory
|
|
239
|
+
lines.append("---\n")
|
|
240
|
+
lines.append("## Metadata Inventory\n")
|
|
241
|
+
lines.append("| Type | Org-Native | Namespaced | Total |")
|
|
242
|
+
lines.append("|------|-----------|------------|-------|")
|
|
243
|
+
for mtype, counts in sorted(meta["by_type"].items()):
|
|
244
|
+
if counts["total"] > 0:
|
|
245
|
+
lines.append(f"| {mtype} | {counts['org_native']} | {counts['namespaced']} | {counts['total']} |")
|
|
246
|
+
lines.append(f"| **Total** | **{meta['total_org_native']}** | **{meta['total_namespaced']}** | **{meta['total_org_native'] + meta['total_namespaced']}** |")
|
|
247
|
+
lines.append("")
|
|
248
|
+
|
|
249
|
+
# Detailed metadata listing
|
|
250
|
+
lines.append("---\n")
|
|
251
|
+
lines.append("## Metadata Components by Type\n")
|
|
252
|
+
for mtype, counts in sorted(meta["by_type"].items()):
|
|
253
|
+
if counts["total"] == 0:
|
|
254
|
+
continue
|
|
255
|
+
lines.append(f"#### {mtype} ({counts['org_native']} org-native, {counts['namespaced']} namespaced)\n")
|
|
256
|
+
if counts["org_native_names"]:
|
|
257
|
+
lines.append(f"**Org-native ({counts['org_native']}):** " +
|
|
258
|
+
", ".join(f"`{n}`" for n in counts["org_native_names"]))
|
|
259
|
+
lines.append("")
|
|
260
|
+
if counts["namespaced_by_ns"]:
|
|
261
|
+
for ns, names in sorted(counts["namespaced_by_ns"].items()):
|
|
262
|
+
lines.append(f"**Namespaced — `{ns}` ({len(names)}):** " +
|
|
263
|
+
", ".join(f"`{n}`" for n in names))
|
|
264
|
+
lines.append("")
|
|
265
|
+
lines.append("")
|
|
266
|
+
|
|
267
|
+
# Installed packages
|
|
268
|
+
lines.append("---\n")
|
|
269
|
+
lines.append("## Installed Packages\n")
|
|
270
|
+
if pkgs["packages"]:
|
|
271
|
+
lines.append("| Package | Namespace | Version |")
|
|
272
|
+
lines.append("|---------|-----------|---------|")
|
|
273
|
+
for p in pkgs["packages"]:
|
|
274
|
+
lines.append(f"| {p['name']} | {p.get('namespace') or '—'} | {p['version']} |")
|
|
275
|
+
else:
|
|
276
|
+
lines.append("No installed packages.\n")
|
|
277
|
+
lines.append("")
|
|
278
|
+
|
|
279
|
+
# Org settings by category
|
|
280
|
+
lines.append("---\n")
|
|
281
|
+
lines.append("## Org Settings\n")
|
|
282
|
+
lines.append("| Category | Enabled | Disabled | Non-Boolean |")
|
|
283
|
+
lines.append("|----------|---------|----------|-------------|")
|
|
284
|
+
for cat in sorted(perms["by_category"].keys()):
|
|
285
|
+
info = perms["by_category"][cat]
|
|
286
|
+
lines.append(f"| {cat} | {info['enabled']} | {info['disabled']} | {info['non_boolean']} |")
|
|
287
|
+
lines.append(f"| **Total** | **{perms['enabled']}** | **{perms['disabled']}** | **{perms['non_boolean']}** |")
|
|
288
|
+
lines.append("")
|
|
289
|
+
|
|
290
|
+
# Org limits by category
|
|
291
|
+
lines.append("---\n")
|
|
292
|
+
lines.append("## Org Limits\n")
|
|
293
|
+
for cat in sorted(vals["by_category"].keys()):
|
|
294
|
+
items = vals["by_category"][cat]
|
|
295
|
+
lines.append(f"### {cat}\n")
|
|
296
|
+
lines.append("| Limit | Value |")
|
|
297
|
+
lines.append("|-------|-------|")
|
|
298
|
+
for name, val in sorted(items):
|
|
299
|
+
lines.append(f"| {name} | {val} |")
|
|
300
|
+
lines.append("")
|
|
301
|
+
|
|
302
|
+
# Licenses
|
|
303
|
+
lines.append("---\n")
|
|
304
|
+
lines.append("## Licenses\n")
|
|
305
|
+
for category in ("user", "permission_set", "package"):
|
|
306
|
+
info = lics[category]
|
|
307
|
+
title = category.replace("_", " ").title()
|
|
308
|
+
lines.append(f"### {title} Licenses ({info['count']})\n")
|
|
309
|
+
if info["items"]:
|
|
310
|
+
sample = info["items"][0]
|
|
311
|
+
if isinstance(sample, dict):
|
|
312
|
+
keys = [k for k in sample.keys() if k not in ("attributes",)]
|
|
313
|
+
if keys:
|
|
314
|
+
lines.append("| " + " | ".join(keys) + " |")
|
|
315
|
+
lines.append("| " + " | ".join(["---"] * len(keys)) + " |")
|
|
316
|
+
for item in info["items"]:
|
|
317
|
+
vals_row = []
|
|
318
|
+
for k in keys:
|
|
319
|
+
v = item.get(k, "")
|
|
320
|
+
if k in ("AllowedLicenses", "TotalLicenses") and v == -1:
|
|
321
|
+
v = "-1 (unlimited)"
|
|
322
|
+
vals_row.append(str(v))
|
|
323
|
+
lines.append("| " + " | ".join(vals_row) + " |")
|
|
324
|
+
else:
|
|
325
|
+
for item in info["items"]:
|
|
326
|
+
lines.append(f"- {item}")
|
|
327
|
+
else:
|
|
328
|
+
lines.append("None.\n")
|
|
329
|
+
lines.append("")
|
|
330
|
+
|
|
331
|
+
# System permissions
|
|
332
|
+
lines.append("---\n")
|
|
333
|
+
lines.append("## System Permissions\n")
|
|
334
|
+
lines.append(f"**{sys_perms['permission_sets']}** permission sets, **{sys_perms['total_enabled']}** total enabled permissions.\n")
|
|
335
|
+
if sys_perms["sets"]:
|
|
336
|
+
lines.append("| Permission Set | Enabled | Total Fields |")
|
|
337
|
+
lines.append("|---------------|---------|-------------|")
|
|
338
|
+
for s in sorted(sys_perms["sets"], key=lambda x: x["enabled"], reverse=True):
|
|
339
|
+
lines.append(f"| {s['name']} | {s['enabled']} | {s['total']} |")
|
|
340
|
+
lines.append("")
|
|
341
|
+
|
|
342
|
+
# Deep data
|
|
343
|
+
lines.append("---\n")
|
|
344
|
+
lines.append("## Deep Data\n")
|
|
345
|
+
lines.append("| Category | Records |")
|
|
346
|
+
lines.append("|----------|---------|")
|
|
347
|
+
for cat, count in sorted(deep.items()):
|
|
348
|
+
lines.append(f"| {cat} | {count} |")
|
|
349
|
+
lines.append(f"| **Total** | **{deep_total}** |")
|
|
350
|
+
lines.append("")
|
|
351
|
+
|
|
352
|
+
# Collection warnings
|
|
353
|
+
if errors:
|
|
354
|
+
lines.append("---\n")
|
|
355
|
+
lines.append("## Collection Warnings\n")
|
|
356
|
+
for err in errors:
|
|
357
|
+
lines.append(f"- {err}")
|
|
358
|
+
lines.append("")
|
|
359
|
+
|
|
360
|
+
lines.append("---\n")
|
|
361
|
+
lines.append("*Report generated by dx-org-analyze. Read-only — no changes were made to the org.*\n")
|
|
362
|
+
|
|
363
|
+
return "\n".join(lines)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── Main ────────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
def main():
|
|
369
|
+
parser = argparse.ArgumentParser(description="Generate a single-org introspection report.")
|
|
370
|
+
parser.add_argument("--org", required=True, help="Path to org data directory or JSON file (from collect_org_data.py)")
|
|
371
|
+
parser.add_argument("--output", required=True, help="Output path prefix (writes .json and/or .md)")
|
|
372
|
+
parser.add_argument("--format", choices=["markdown", "json", "both"], default="both",
|
|
373
|
+
help="Output format (default: both)")
|
|
374
|
+
parser.add_argument("--label", default=None, help="Display name for the org (default: org name from data)")
|
|
375
|
+
args = parser.parse_args()
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
data = load_org_data(args.org)
|
|
379
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
380
|
+
print(f"Error loading org data: {e}", file=sys.stderr)
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
label = args.label or data.get("org_name", "Unknown Org")
|
|
384
|
+
|
|
385
|
+
meta = analyze_metadata(data)
|
|
386
|
+
perms = analyze_permissions(data)
|
|
387
|
+
vals = analyze_org_values(data)
|
|
388
|
+
pkgs = analyze_packages(data)
|
|
389
|
+
lics = analyze_licenses(data)
|
|
390
|
+
sys_perms = analyze_system_permissions(data)
|
|
391
|
+
deep = analyze_deep_data(data)
|
|
392
|
+
|
|
393
|
+
result = {
|
|
394
|
+
"org_id": data.get("org_id"),
|
|
395
|
+
"org_name": data.get("org_name"),
|
|
396
|
+
"org_edition": data.get("org_edition"),
|
|
397
|
+
"label": label,
|
|
398
|
+
"metadata": meta,
|
|
399
|
+
"permissions": perms,
|
|
400
|
+
"org_values": vals,
|
|
401
|
+
"packages": pkgs,
|
|
402
|
+
"licenses": lics,
|
|
403
|
+
"system_permissions": sys_perms,
|
|
404
|
+
"deep_data": deep,
|
|
405
|
+
"errors": data.get("errors", []),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if args.format in ("json", "both"):
|
|
409
|
+
json_path = args.output + ".json"
|
|
410
|
+
serializable = json.loads(json.dumps(result, default=str))
|
|
411
|
+
with open(json_path, "w") as f:
|
|
412
|
+
json.dump(serializable, f, indent=2)
|
|
413
|
+
print(f"Wrote {json_path}")
|
|
414
|
+
|
|
415
|
+
if args.format in ("markdown", "both"):
|
|
416
|
+
md_path = args.output + ".md"
|
|
417
|
+
md = render_markdown(data, meta, perms, vals, pkgs, lics, sys_perms, deep, label)
|
|
418
|
+
with open(md_path, "w") as f:
|
|
419
|
+
f.write(md)
|
|
420
|
+
print(f"Wrote {md_path}")
|
|
421
|
+
|
|
422
|
+
# Print summary
|
|
423
|
+
print(f"\n# Org Analysis Report: {label}")
|
|
424
|
+
print(f" Edition: {data.get('org_edition', 'N/A')}")
|
|
425
|
+
print(f" Metadata: {meta['types_discovered']} types, {meta['total_org_native']} org-native + {meta['total_namespaced']} namespaced components")
|
|
426
|
+
print(f" Org settings: {perms['total']} ({perms['enabled']} enabled)")
|
|
427
|
+
print(f" Org limits: {vals['total']}")
|
|
428
|
+
print(f" Packages: {pkgs['count']}")
|
|
429
|
+
print(f" Licenses: {lics['user']['count']} user, {lics['permission_set']['count']} PSL, {lics['package']['count']} package")
|
|
430
|
+
print(f" System permissions: {sys_perms['permission_sets']} sets, {sys_perms['total_enabled']} enabled")
|
|
431
|
+
deep_total = sum(deep.values())
|
|
432
|
+
print(f" Deep data: {deep_total} records")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# dx-org-analyze Tests
|
|
2
|
+
|
|
3
|
+
Structural consistency tests and diff computation tests for the `dx-org-analyze` skill.
|
|
4
|
+
|
|
5
|
+
## What's covered
|
|
6
|
+
|
|
7
|
+
| Test | What it checks |
|
|
8
|
+
|------|----------------|
|
|
9
|
+
| `test_consistency.sh` | Frontmatter fields, SF CLI auth documented, drift formula, severity levels, public API usage, no internal tool references, core rules |
|
|
10
|
+
| `test_compute_diff.sh` | Diff computation against fixture data: metadata, permissions, org values, packages, licenses, markdown report structure |
|
|
11
|
+
|
|
12
|
+
## What's not covered
|
|
13
|
+
|
|
14
|
+
- **Live org comparison** — requires two authenticated orgs. Keep as manual smoke test.
|
|
15
|
+
- **Drift calculation accuracy** — verify manually against known org pairs.
|
|
16
|
+
- **Package comparison** — requires real managed packages on the instance.
|
|
17
|
+
- **OrganizationSettingsDetail** — not available on all editions; test graceful fallback manually.
|
|
18
|
+
|
|
19
|
+
## Running
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Run all tests
|
|
23
|
+
bash tests/test_consistency.sh && bash tests/test_compute_diff.sh
|
|
24
|
+
|
|
25
|
+
# Run individually
|
|
26
|
+
bash tests/test_consistency.sh # Skill structure tests
|
|
27
|
+
bash tests/test_compute_diff.sh # Diff computation tests
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Exit code `0` on pass, `1` on fail.
|
|
31
|
+
|
|
32
|
+
## Manual smoke checklist
|
|
33
|
+
|
|
34
|
+
- [ ] Triggers on "compare these two orgs"
|
|
35
|
+
- [ ] Lists authenticated orgs from `sf org list`
|
|
36
|
+
- [ ] Asks user to pick source and target
|
|
37
|
+
- [ ] Handles `sf org login web` when org not authenticated
|
|
38
|
+
- [ ] Report order: Drift Score → Summary → Details
|
|
39
|
+
- [ ] Drift percentages are 0-100%
|
|
40
|
+
- [ ] Shared components show field/body-level diff
|
|
41
|
+
- [ ] Permissions categorized correctly
|
|
42
|
+
- [ ] Handles 0 packages gracefully
|
|
43
|
+
- [ ] Handles different editions without breaking
|
|
44
|
+
- [ ] Gracefully degrades when OrganizationSettingsDetail unavailable
|
|
45
|
+
- [ ] Falls back to SecurityHealthCheckRisks for settings
|
|
46
|
+
- [ ] REST /limits/ endpoint populates org values
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"org_id": "00Dxx0000000001",
|
|
3
|
+
"org_name": "TestOrgA",
|
|
4
|
+
"org_edition": "Developer Edition",
|
|
5
|
+
"base_url": "https://org-a.example.com",
|
|
6
|
+
"collected_at": "2026-05-13T00:00:00Z",
|
|
7
|
+
"api_version": "62.0",
|
|
8
|
+
"metadata_types": ["ApexClass", "CustomObject", "Profile"],
|
|
9
|
+
"metadata_components": {
|
|
10
|
+
"ApexClass": [
|
|
11
|
+
{"fullName": "SharedClass", "namespace": null},
|
|
12
|
+
{"fullName": "OnlyInA", "namespace": null},
|
|
13
|
+
{"fullName": "ManagedPkg", "namespace": "pkg"}
|
|
14
|
+
],
|
|
15
|
+
"CustomObject": [
|
|
16
|
+
{"fullName": "Project__c", "namespace": null}
|
|
17
|
+
],
|
|
18
|
+
"Profile": [
|
|
19
|
+
{"fullName": "Admin", "namespace": null},
|
|
20
|
+
{"fullName": "Standard User", "namespace": null},
|
|
21
|
+
{"fullName": "Custom Profile", "namespace": null}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"permissions": {
|
|
25
|
+
"ApiEnabled": {"value": "true"},
|
|
26
|
+
"ApexRest": {"value": "true"},
|
|
27
|
+
"SharedPerm": {"value": "true"},
|
|
28
|
+
"DisabledInA": {"value": "false"}
|
|
29
|
+
},
|
|
30
|
+
"org_values": {
|
|
31
|
+
"OrgEdition": "DE",
|
|
32
|
+
"MaxCustomObjects": "400",
|
|
33
|
+
"DataStorage": "5",
|
|
34
|
+
"SharedValue": "same"
|
|
35
|
+
},
|
|
36
|
+
"licenses": {
|
|
37
|
+
"user": [
|
|
38
|
+
{"Name": "Salesforce", "TotalLicenses": 10, "UsedLicenses": 1, "Status": "Active"},
|
|
39
|
+
{"Name": "Chatter Free", "TotalLicenses": 5000, "UsedLicenses": 0, "Status": "Active"},
|
|
40
|
+
{"Name": "OnlyInA License", "TotalLicenses": 5, "UsedLicenses": 0, "Status": "Active"}
|
|
41
|
+
],
|
|
42
|
+
"permission_set": [
|
|
43
|
+
{"DeveloperName": "SalesforceCPQ", "TotalLicenses": 10, "UsedLicenses": 0}
|
|
44
|
+
],
|
|
45
|
+
"package": []
|
|
46
|
+
},
|
|
47
|
+
"installed_packages": [
|
|
48
|
+
{"name": "Shared Package", "namespace": "sharedpkg", "version": "1.5"},
|
|
49
|
+
{"name": "Only In A", "namespace": "apkg", "version": "2.0"},
|
|
50
|
+
{"name": "Version Mismatch", "namespace": "vmpkg", "version": "3.1"}
|
|
51
|
+
],
|
|
52
|
+
"system_permissions": {
|
|
53
|
+
"fields_discovered": ["PermissionsApiEnabled", "PermissionsAuthorApex", "PermissionsBulkApiHardDelete", "PermissionsManageUsers", "PermissionsViewSetup"],
|
|
54
|
+
"permission_sets": [
|
|
55
|
+
{
|
|
56
|
+
"name": "Admin",
|
|
57
|
+
"label": "System Administrator",
|
|
58
|
+
"is_profile": true,
|
|
59
|
+
"permissions": {"PermissionsApiEnabled": true, "PermissionsAuthorApex": true, "PermissionsManageUsers": true, "PermissionsViewSetup": true}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "StandardUser",
|
|
63
|
+
"label": "Standard User",
|
|
64
|
+
"is_profile": true,
|
|
65
|
+
"permissions": {"PermissionsApiEnabled": true, "PermissionsViewSetup": true}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "OnlyInAPermSet",
|
|
69
|
+
"label": "Only In A PermSet",
|
|
70
|
+
"is_profile": false,
|
|
71
|
+
"permissions": {"PermissionsBulkApiHardDelete": true}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"errors": []
|
|
76
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"org_id": "00Dxx0000000002",
|
|
3
|
+
"org_name": "TestOrgB",
|
|
4
|
+
"org_edition": "Professional Edition",
|
|
5
|
+
"base_url": "https://org-b.example.com",
|
|
6
|
+
"collected_at": "2026-05-13T00:00:00Z",
|
|
7
|
+
"api_version": "62.0",
|
|
8
|
+
"metadata_types": ["ApexClass", "Profile"],
|
|
9
|
+
"metadata_components": {
|
|
10
|
+
"ApexClass": [
|
|
11
|
+
{"fullName": "SharedClass", "namespace": null},
|
|
12
|
+
{"fullName": "OnlyInB", "namespace": null}
|
|
13
|
+
],
|
|
14
|
+
"Profile": [
|
|
15
|
+
{"fullName": "Admin", "namespace": null},
|
|
16
|
+
{"fullName": "Standard User", "namespace": null}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"permissions": {
|
|
20
|
+
"SharedPerm": {"value": "true"},
|
|
21
|
+
"DisabledInA": {"value": "true"},
|
|
22
|
+
"OnlyInB": {"value": "true"}
|
|
23
|
+
},
|
|
24
|
+
"org_values": {
|
|
25
|
+
"OrgEdition": "PE",
|
|
26
|
+
"MaxCustomObjects": "50",
|
|
27
|
+
"DataStorage": "10240",
|
|
28
|
+
"SharedValue": "same"
|
|
29
|
+
},
|
|
30
|
+
"licenses": {
|
|
31
|
+
"user": [
|
|
32
|
+
{"Name": "Salesforce", "TotalLicenses": 25, "UsedLicenses": 3, "Status": "Active"},
|
|
33
|
+
{"Name": "Chatter Free", "TotalLicenses": 5000, "UsedLicenses": 0, "Status": "Active"},
|
|
34
|
+
{"Name": "OnlyInB License", "TotalLicenses": 100, "UsedLicenses": 0, "Status": "Active"}
|
|
35
|
+
],
|
|
36
|
+
"permission_set": [
|
|
37
|
+
{"DeveloperName": "SalesforceCPQ", "TotalLicenses": 25, "UsedLicenses": 0}
|
|
38
|
+
],
|
|
39
|
+
"package": []
|
|
40
|
+
},
|
|
41
|
+
"installed_packages": [
|
|
42
|
+
{"name": "Shared Package", "namespace": "sharedpkg", "version": "1.5"},
|
|
43
|
+
{"name": "Only In B", "namespace": "bpkg", "version": "1.0"},
|
|
44
|
+
{"name": "Version Mismatch", "namespace": "vmpkg", "version": "4.0"}
|
|
45
|
+
],
|
|
46
|
+
"system_permissions": {
|
|
47
|
+
"fields_discovered": ["PermissionsApiEnabled", "PermissionsAuthorApex", "PermissionsBulkApiHardDelete", "PermissionsManageUsers", "PermissionsViewSetup"],
|
|
48
|
+
"permission_sets": [
|
|
49
|
+
{
|
|
50
|
+
"name": "Admin",
|
|
51
|
+
"label": "System Administrator",
|
|
52
|
+
"is_profile": true,
|
|
53
|
+
"permissions": {"PermissionsApiEnabled": true, "PermissionsAuthorApex": true, "PermissionsManageUsers": true, "PermissionsViewSetup": true, "PermissionsBulkApiHardDelete": true}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "StandardUser",
|
|
57
|
+
"label": "Standard User",
|
|
58
|
+
"is_profile": true,
|
|
59
|
+
"permissions": {"PermissionsApiEnabled": true}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "OnlyInBPermSet",
|
|
63
|
+
"label": "Only In B PermSet",
|
|
64
|
+
"is_profile": false,
|
|
65
|
+
"permissions": {"PermissionsManageUsers": true}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"errors": []
|
|
70
|
+
}
|