Setting the file. One moment. CLI · Azure Architecture Autopilot · github/awesome-copilot · Skills Docsscripts/cli.py
Python·161 lines·6 KB
generator
import
generate_diagram
13
14
15def main():
16 parser = argparse.ArgumentParser(
17 description="Generate interactive Azure architecture diagrams",
18 prog="azure-architecture-autopilot"
19 )
20 parser.add_argument("-s", "--services", help="Services JSON (string or file path)")
21 parser.add_argument("-c", "--connections", help="Connections JSON (string or file path)")
22 parser.add_argument("-t", "--title", default="Azure Architecture", help="Diagram title")
23 parser.add_argument("-o", "--output", default="azure-architecture.html", help="Output file path")
24 parser.add_argument("-f", "--format", choices=["html", "png", "both"], default="html",
25 help="Output format: html (default), png, or both (html+png)")
26 parser.add_argument("--vnet-info", default="", help="VNet CIDR info")
27 parser.add_argument("--hierarchy", default="", help="Subscription/RG hierarchy JSON")
28
29 args = parser.parse_args()
30
31 if not args.services or not args.connections:
32 parser.error("-s/--services and -c/--connections are required")
33
34 services = _load_json(args.services, "services")
35 connections = _load_json(args.connections, "connections")
36 hierarchy = None
37 if args.hierarchy:
38 hierarchy = _load_json(args.hierarchy, "hierarchy")
39
40 services = _normalize_services(services)
41 connections = _normalize_connections(connections)
42
43 html = generate_diagram(
44 services=services,
45 connections=connections,
46 title=args.title,
47 vnet_info=args.vnet_info,
48 hierarchy=hierarchy,
49 )
50
51 # Determine output paths
52 out = Path(args.output)
53 html_path = out.with_suffix(".html")
54 png_path = out.with_suffix(".png")
55 svg_path = out.with_suffix(".svg")
56
57 if args.format in ("html", "both"):
58 html_path.write_text(html, encoding="utf-8")
59 print(f"HTML saved: {html_path}")
60
61 if args.format in ("png", "both"):
62 # Write temp HTML then screenshot with puppeteer/playwright
63 tmp_html = html_path if args.format == "both" else Path(str(png_path) + ".tmp.html")
64 if args.format != "both":
65 tmp_html.write_text(html, encoding="utf-8")
66
67 success = _html_to_png(tmp_html, png_path)
68
69 if args.format != "both" and tmp_html.exists():
70 tmp_html.unlink()
71
72 if success:
73 print(f"PNG saved: {png_path}")
74 else:
75 print(f"WARNING: PNG export failed. Install puppeteer (npm i puppeteer) for PNG support.", file=sys.stderr)
76 print(f"HTML saved instead: {html_path}")
77 if not html_path.exists():
78 html_path.write_text(html, encoding="utf-8")
79
80
81def _html_to_png(html_path, png_path, width=1920, height=1080):
82 """Convert HTML to PNG using puppeteer (Node.js)."""
83 node = shutil.which("node")
84 if not node:
85 return False
86
87 # Try multiple puppeteer locations
88 script = f"""
89let puppeteer;
90const paths = [
91 'puppeteer',
92 process.env.TEMP + '/node_modules/puppeteer',
93 process.env.HOME + '/node_modules/puppeteer',
94 './node_modules/puppeteer'
95];
96for (const p of paths) {{ try {{ puppeteer = require(p); break; }} catch(e) {{}} }}
97if (!puppeteer) {{ console.error('puppeteer not found'); process.exit(1); }}
98(async () => {{
99 const browser = await puppeteer.launch({{headless: 'new'}});
100 const page = await browser.newPage();
101 await page.setViewport({{width: {width}, height: {height}}});
102 await page.goto('file:///{html_path.resolve().as_posix()}', {{waitUntil: 'networkidle0'}});
103 await new Promise(r => setTimeout(r, 2000));
104 await page.screenshot({{path: '{png_path.resolve().as_posix()}'}});
105 await browser.close();
106}})();
107"""
108 try:
109 result = subprocess.run([node, "-e", script], capture_output=True, text=True, timeout=30)
110 return result.returncode == 0 and png_path.exists()
111 except (subprocess.TimeoutExpired, FileNotFoundError):
112 return False
113
114
115def _load_json(value, name):
116 """Load JSON from string or file path. Extracts named key from combined JSON if present."""
117 data = None
118 if os.path.isfile(value):
119 with open(value, "r", encoding="utf-8") as f:
120 data = json.load(f)
121 else:
122 try:
123 data = json.loads(value)
124 except json.JSONDecodeError as e:
125 print(f"ERROR: Invalid JSON for --{name}: {e}", file=sys.stderr)
126 sys.exit(1)
127
128 # If data is a dict with the named key, extract it (combined JSON file support)
129 if isinstance(data, dict) and name in data:
130 return data[name]
131 return data
132
133
134def _normalize_services(services):
135 """Normalize service fields for tolerance."""
136 for svc in services:
137 if isinstance(svc.get("details"), str):
138 svc["details"] = [svc["details"]]
139 if isinstance(svc.get("private"), str):
140 val = svc["private"].lower()
141 if val in ("true", "1", "yes", "on"):
142 svc["private"] = True
143 elif val in ("false", "0", "no", "off"):
144 svc["private"] = False
145 else:
146 # Log warning for invalid values
147 print(f"WARNING: Invalid boolean value '{svc['private']}' for 'private' field. Defaulting to False.", file=sys.stderr)
148 svc["private"] = False
149 return services
150
151
152def _normalize_connections(connections):
153 """Normalize connection fields for tolerance."""
154 for conn in connections:
155 if "type" not in conn:
156 conn["type"] = "default"
157 return connections
158
159
160if __name__ == "__main__":
161 main()