lvkit 0.1.0__py3-none-any.whl

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.
Files changed (128) hide show
  1. lvkit/__init__.py +55 -0
  2. lvkit/_data.py +24 -0
  3. lvkit/cli.py +980 -0
  4. lvkit/codegen/README.md +147 -0
  5. lvkit/codegen/__init__.py +36 -0
  6. lvkit/codegen/ast_optimizer.py +436 -0
  7. lvkit/codegen/ast_utils.py +241 -0
  8. lvkit/codegen/builder.py +754 -0
  9. lvkit/codegen/class_builder.py +775 -0
  10. lvkit/codegen/condition_builder.py +316 -0
  11. lvkit/codegen/context.py +576 -0
  12. lvkit/codegen/dataflow.py +241 -0
  13. lvkit/codegen/error_handler.py +297 -0
  14. lvkit/codegen/expressions.py +202 -0
  15. lvkit/codegen/fragment.py +54 -0
  16. lvkit/codegen/function.py +307 -0
  17. lvkit/codegen/imports.py +132 -0
  18. lvkit/codegen/nodes/__init__.py +102 -0
  19. lvkit/codegen/nodes/base.py +50 -0
  20. lvkit/codegen/nodes/case.py +345 -0
  21. lvkit/codegen/nodes/compound.py +159 -0
  22. lvkit/codegen/nodes/constant.py +33 -0
  23. lvkit/codegen/nodes/invoke_node.py +83 -0
  24. lvkit/codegen/nodes/loop.py +744 -0
  25. lvkit/codegen/nodes/nmux.py +224 -0
  26. lvkit/codegen/nodes/primitive.py +673 -0
  27. lvkit/codegen/nodes/printf.py +70 -0
  28. lvkit/codegen/nodes/property_node.py +106 -0
  29. lvkit/codegen/nodes/sequence.py +65 -0
  30. lvkit/codegen/nodes/subvi.py +789 -0
  31. lvkit/codegen/stubs.py +184 -0
  32. lvkit/codegen/unresolved.py +156 -0
  33. lvkit/data/drivers/_index.json +15 -0
  34. lvkit/data/drivers/daqmx.json +2708 -0
  35. lvkit/data/drivers/nidcpower.json +545 -0
  36. lvkit/data/drivers/nidigital.json +830 -0
  37. lvkit/data/drivers/nidmm.json +620 -0
  38. lvkit/data/drivers/nifgen.json +490 -0
  39. lvkit/data/drivers/niscope.json +560 -0
  40. lvkit/data/drivers/niswitch.json +475 -0
  41. lvkit/data/drivers/serial.json +170 -0
  42. lvkit/data/drivers/visa.json +542 -0
  43. lvkit/data/labview_error_codes.json +1318 -0
  44. lvkit/data/openg/_index.json +12 -0
  45. lvkit/data/openg/array.json +6951 -0
  46. lvkit/data/openg/file.json +876 -0
  47. lvkit/data/openg/string.json +146 -0
  48. lvkit/data/openg/time.json +98 -0
  49. lvkit/data/openg/variant.json +33 -0
  50. lvkit/data/primitives-from-pdf.json +8452 -0
  51. lvkit/data/primitives.json +3166 -0
  52. lvkit/data/vilib/_index.json +19 -0
  53. lvkit/data/vilib/_pending_terminals.json +1391 -0
  54. lvkit/data/vilib/_types.json +24 -0
  55. lvkit/data/vilib/application-control.json +5408 -0
  56. lvkit/data/vilib/array.json +2071 -0
  57. lvkit/data/vilib/boolean.json +704 -0
  58. lvkit/data/vilib/cluster.json +776 -0
  59. lvkit/data/vilib/comparison.json +2388 -0
  60. lvkit/data/vilib/error-handling.json +2274 -0
  61. lvkit/data/vilib/file-io.json +4733 -0
  62. lvkit/data/vilib/numeric.json +1538 -0
  63. lvkit/data/vilib/other.json +87782 -0
  64. lvkit/data/vilib/string.json +3582 -0
  65. lvkit/data/vilib/structures.json +1040 -0
  66. lvkit/data/vilib/variant.json +1343 -0
  67. lvkit/docs/__init__.py +1 -0
  68. lvkit/docs/generate.py +400 -0
  69. lvkit/docs/html_generator.py +853 -0
  70. lvkit/docs/template.css +398 -0
  71. lvkit/docs/utils.py +109 -0
  72. lvkit/extractor.py +98 -0
  73. lvkit/graph/__init__.py +10 -0
  74. lvkit/graph/analysis.py +158 -0
  75. lvkit/graph/construction.py +1221 -0
  76. lvkit/graph/core.py +321 -0
  77. lvkit/graph/describe.py +493 -0
  78. lvkit/graph/diff.py +387 -0
  79. lvkit/graph/flowchart.py +281 -0
  80. lvkit/graph/loading.py +716 -0
  81. lvkit/graph/models.py +420 -0
  82. lvkit/graph/operations.py +446 -0
  83. lvkit/graph/queries.py +813 -0
  84. lvkit/labview_error.py +146 -0
  85. lvkit/labview_error_codes.py +48 -0
  86. lvkit/mcp/__init__.py +7 -0
  87. lvkit/mcp/schemas.py +239 -0
  88. lvkit/mcp/server.py +647 -0
  89. lvkit/mcp/tools.py +204 -0
  90. lvkit/models.py +388 -0
  91. lvkit/parser/__init__.py +96 -0
  92. lvkit/parser/constants.py +158 -0
  93. lvkit/parser/defaults.py +117 -0
  94. lvkit/parser/flags.py +26 -0
  95. lvkit/parser/front_panel.py +257 -0
  96. lvkit/parser/metadata.py +290 -0
  97. lvkit/parser/models.py +406 -0
  98. lvkit/parser/naming.py +77 -0
  99. lvkit/parser/node_types.py +600 -0
  100. lvkit/parser/nodes/__init__.py +16 -0
  101. lvkit/parser/nodes/base.py +80 -0
  102. lvkit/parser/nodes/case.py +389 -0
  103. lvkit/parser/nodes/constant.py +55 -0
  104. lvkit/parser/nodes/loop.py +134 -0
  105. lvkit/parser/nodes/sequence.py +150 -0
  106. lvkit/parser/type_mapping.py +387 -0
  107. lvkit/parser/type_resolution.py +254 -0
  108. lvkit/parser/utils.py +124 -0
  109. lvkit/parser/vi.py +1033 -0
  110. lvkit/pipeline.py +683 -0
  111. lvkit/primitive_resolver.py +633 -0
  112. lvkit/project_store.py +480 -0
  113. lvkit/py.typed +0 -0
  114. lvkit/skill_templates/__init__.py +15 -0
  115. lvkit/skill_templates/lvkit-convert/SKILL.md +141 -0
  116. lvkit/skill_templates/lvkit-describe/SKILL.md +108 -0
  117. lvkit/skill_templates/lvkit-idiomatic/SKILL.md +85 -0
  118. lvkit/skill_templates/lvkit-resolve-primitive/SKILL.md +275 -0
  119. lvkit/skill_templates/lvkit-resolve-vilib/SKILL.md +146 -0
  120. lvkit/structure.py +788 -0
  121. lvkit/terminal_collector.py +295 -0
  122. lvkit/type_defaults.py +195 -0
  123. lvkit/vilib_resolver.py +1160 -0
  124. lvkit-0.1.0.dist-info/METADATA +199 -0
  125. lvkit-0.1.0.dist-info/RECORD +128 -0
  126. lvkit-0.1.0.dist-info/WHEEL +4 -0
  127. lvkit-0.1.0.dist-info/entry_points.txt +3 -0
  128. lvkit-0.1.0.dist-info/licenses/LICENSE +201 -0
lvkit/cli.py ADDED
@@ -0,0 +1,980 @@
1
+ """Command-line interface for lvkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import traceback
9
+ from pathlib import Path
10
+
11
+ from . import __version__, primitive_resolver, vilib_resolver
12
+ from .graph import InMemoryVIGraph
13
+ from .project_store import (
14
+ find_project_store,
15
+ init_project_store,
16
+ install_claude_skills,
17
+ install_copilot_skills,
18
+ )
19
+ from .structure import (
20
+ discover_project_structure,
21
+ generate_python_structure_plan,
22
+ parse_lvclass,
23
+ parse_lvlib,
24
+ )
25
+
26
+
27
+ def _add_project_root_arg(parser: argparse.ArgumentParser) -> None:
28
+ """Add --project-root flag to a subparser."""
29
+ parser.add_argument(
30
+ "--project-root",
31
+ default=None,
32
+ metavar="DIR",
33
+ help=(
34
+ "Project root containing a .lvkit/ resolution store. "
35
+ "Defaults to walking up from CWD looking for .lvkit/."
36
+ ),
37
+ )
38
+
39
+
40
+ def _configure_resolvers(args: argparse.Namespace) -> Path | None:
41
+ """Discover the project store and reset resolver singletons.
42
+
43
+ Must be called BEFORE any load_vi() so graph construction sees the
44
+ project mappings (used for terminal-index disambiguation).
45
+
46
+ Accepts --project-root in either form: the parent of .lvkit/ (the
47
+ project root), or the .lvkit/ directory itself.
48
+
49
+ Returns the project store directory if one was found, else None.
50
+ """
51
+ project_root = getattr(args, "project_root", None)
52
+ store: Path | None
53
+ if project_root:
54
+ candidate = Path(project_root)
55
+ # Accept both "project root" and ".lvkit/" itself
56
+ if candidate.name == ".lvkit" and candidate.is_dir():
57
+ store = candidate
58
+ elif (candidate / ".lvkit").is_dir():
59
+ store = candidate / ".lvkit"
60
+ else:
61
+ store = None
62
+ else:
63
+ store = find_project_store()
64
+
65
+ primitive_resolver.reset_resolver(project_data_dir=store)
66
+ vilib_resolver.reset_resolver(project_data_dir=store)
67
+ return store
68
+
69
+
70
+ def main() -> int:
71
+ """Main CLI entry point."""
72
+ parser = argparse.ArgumentParser(
73
+ prog="lvkit",
74
+ description="Understand, convert, and document LabVIEW VI files.",
75
+ )
76
+ parser.add_argument("--version", action="version", version=f"lvkit {__version__}")
77
+
78
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
79
+
80
+ # Structure command
81
+ struct_parser = subparsers.add_parser(
82
+ "structure", help="Analyze LabVIEW project structure"
83
+ )
84
+ struct_parser.add_argument("input", help="Directory, .lvlib, or .lvclass file")
85
+ struct_parser.add_argument("--json", action="store_true", help="Output as JSON")
86
+ struct_parser.add_argument(
87
+ "--plan", action="store_true", help="Generate Python structure plan"
88
+ )
89
+
90
+ # MCP server command
91
+ subparsers.add_parser(
92
+ "mcp",
93
+ help="Run MCP server for VI analysis",
94
+ )
95
+
96
+ # Describe command - human-readable VI description
97
+ desc_parser = subparsers.add_parser(
98
+ "describe",
99
+ help="Describe a VI's purpose, signature, and structure",
100
+ )
101
+ desc_parser.add_argument(
102
+ "input_path",
103
+ help="Path to .vi file",
104
+ )
105
+ desc_parser.add_argument(
106
+ "--search-path",
107
+ action="append",
108
+ dest="search_paths",
109
+ default=[],
110
+ help="Search paths for SubVI resolution (can be repeated)",
111
+ )
112
+ desc_parser.add_argument(
113
+ "--chart", action="store_true",
114
+ help="Include Mermaid flowchart diagram",
115
+ )
116
+ _add_project_root_arg(desc_parser)
117
+
118
+ # Generate command - deterministic AST-based Python generation
119
+ gen_parser = subparsers.add_parser(
120
+ "generate",
121
+ help="Generate Python from VI files using deterministic AST pipeline",
122
+ )
123
+ gen_parser.add_argument(
124
+ "input_path",
125
+ help="Path to .vi, .lvlib, .lvclass, or directory",
126
+ )
127
+ gen_parser.add_argument(
128
+ "-o", "--output", default="outputs",
129
+ help="Output directory",
130
+ )
131
+ gen_parser.add_argument(
132
+ "--search-path",
133
+ action="append",
134
+ dest="search_paths",
135
+ default=[],
136
+ help="Search paths for SubVI resolution (can be repeated)",
137
+ )
138
+ gen_parser.add_argument(
139
+ "--no-expand", action="store_true",
140
+ help="Don't expand SubVIs",
141
+ )
142
+ # User-facing name is --placeholder-on-unresolved (descriptive of the
143
+ # output the user sees in their generated Python). Internally this
144
+ # flows to CodeGenContext.soft_unresolved (the codegen-time mode).
145
+ gen_parser.add_argument(
146
+ "--placeholder-on-unresolved",
147
+ action="store_true",
148
+ help=(
149
+ "Don't fail on unknown primitives or vi.lib VIs. Instead emit "
150
+ "an inline `raise PrimitiveResolutionNeeded(...)` / `raise "
151
+ "VILibResolutionNeeded(...)` in the generated Python so the "
152
+ "build succeeds and unresolved calls are visible at runtime."
153
+ ),
154
+ )
155
+ _add_project_root_arg(gen_parser)
156
+
157
+ # Docs command - generate HTML documentation
158
+ docs_parser = subparsers.add_parser(
159
+ "docs",
160
+ help="Generate HTML documentation for VI files",
161
+ )
162
+ docs_parser.add_argument(
163
+ "input_path",
164
+ help="Path to .vi, .lvlib, .lvclass, or directory",
165
+ )
166
+ docs_parser.add_argument(
167
+ "output_dir", help="Output directory for HTML files",
168
+ )
169
+ docs_parser.add_argument(
170
+ "--search-path",
171
+ action="append",
172
+ dest="search_paths",
173
+ default=[],
174
+ help="Search paths for SubVI resolution (can be repeated)",
175
+ )
176
+ docs_parser.add_argument(
177
+ "--no-expand", action="store_true",
178
+ help="Don't expand SubVIs",
179
+ )
180
+ _add_project_root_arg(docs_parser)
181
+
182
+ # Visualize command - interactive graph visualization
183
+ viz_parser = subparsers.add_parser(
184
+ "visualize",
185
+ help="Generate VI graphs as Mermaid flowcharts or interactive diagrams",
186
+ )
187
+ viz_parser.add_argument(
188
+ "input_path",
189
+ help="Path to .vi, .lvlib, .lvclass, or directory",
190
+ )
191
+ viz_parser.add_argument(
192
+ "-o", "--output",
193
+ default="outputs/graph.html",
194
+ help="Output HTML file (default: outputs/graph.html)",
195
+ )
196
+ viz_parser.add_argument(
197
+ "--search-path",
198
+ action="append",
199
+ dest="search_paths",
200
+ default=[],
201
+ help="Search paths for SubVI resolution (can be repeated)",
202
+ )
203
+ viz_parser.add_argument(
204
+ "--no-expand", action="store_true",
205
+ help="Don't expand SubVIs",
206
+ )
207
+ viz_parser.add_argument(
208
+ "--open", action="store_true",
209
+ help="Open in browser after generating",
210
+ )
211
+ viz_parser.add_argument(
212
+ "--mode",
213
+ default="dataflow",
214
+ choices=["dataflow", "deps"],
215
+ help="Graph type: dataflow (operations within VI) or deps (VI dependencies)",
216
+ )
217
+ viz_parser.add_argument(
218
+ "--format",
219
+ default=None,
220
+ choices=["interactive", "flowchart"],
221
+ help="Output format: flowchart (Mermaid, default for dataflow) "
222
+ "or interactive (pyvis, default for deps)",
223
+ )
224
+ _add_project_root_arg(viz_parser)
225
+
226
+ # Diff command - compare two VIs
227
+ diff_parser = subparsers.add_parser(
228
+ "diff",
229
+ help="Compare two versions of a VI",
230
+ )
231
+ diff_parser.add_argument(
232
+ "vi_a",
233
+ help="Path to first .vi file",
234
+ )
235
+ diff_parser.add_argument(
236
+ "vi_b",
237
+ help="Path to second .vi file",
238
+ )
239
+ diff_parser.add_argument(
240
+ "--long", action="store_true",
241
+ help="Show structured change report instead of unified diff",
242
+ )
243
+ diff_parser.add_argument(
244
+ "--search-path",
245
+ action="append",
246
+ dest="search_paths",
247
+ default=[],
248
+ help="Search paths for SubVI resolution (can be repeated)",
249
+ )
250
+ _add_project_root_arg(diff_parser)
251
+
252
+ # Init command - create .lvkit/ project store
253
+ init_parser = subparsers.add_parser(
254
+ "init",
255
+ help="Initialize a project-local .lvkit/ resolution store",
256
+ )
257
+ init_parser.add_argument(
258
+ "directory",
259
+ nargs="?",
260
+ default=".",
261
+ help="Directory in which to create .lvkit/ (default: current directory)",
262
+ )
263
+ init_parser.add_argument(
264
+ "--skills",
265
+ choices=["claude", "copilot", "all"],
266
+ default=None,
267
+ help=(
268
+ "Also install lvkit's resolve workflows into your LLM editor: "
269
+ "claude installs lvkit-prefixed Claude Code skills under "
270
+ ".claude/skills/; copilot installs per-workflow prompts under "
271
+ ".github/prompts/ plus a router at "
272
+ ".github/instructions/lvkit.instructions.md; all does both."
273
+ ),
274
+ )
275
+ init_parser.add_argument(
276
+ "--force",
277
+ action="store_true",
278
+ help="Overwrite existing skill files even if they have local edits",
279
+ )
280
+
281
+ args = parser.parse_args()
282
+
283
+ if args.command == "structure":
284
+ return cmd_structure(args)
285
+ elif args.command == "mcp":
286
+ return cmd_mcp(args)
287
+ elif args.command == "describe":
288
+ return cmd_describe(args)
289
+ elif args.command == "generate":
290
+ return cmd_generate(args)
291
+ elif args.command == "docs":
292
+ return cmd_docs(args)
293
+ elif args.command == "visualize":
294
+ return cmd_visualize(args)
295
+ elif args.command == "diff":
296
+ return cmd_diff(args)
297
+ elif args.command == "init":
298
+ return cmd_init(args)
299
+ else:
300
+ parser.print_help()
301
+ return 0
302
+
303
+
304
+ def cmd_structure(args: argparse.Namespace) -> int:
305
+ """Handle the structure command."""
306
+ input_path = Path(args.input)
307
+
308
+ if not input_path.exists():
309
+ print(f"Error: Path not found: {input_path}", file=sys.stderr)
310
+ return 1
311
+
312
+ try:
313
+ if input_path.suffix == ".lvclass":
314
+ # Single class
315
+ cls = parse_lvclass(input_path)
316
+ if args.json:
317
+ data = {
318
+ "name": cls.name,
319
+ "path": str(cls.path),
320
+ "parent_class": cls.parent_class,
321
+ "private_data": cls.private_data_ctl,
322
+ "methods": [
323
+ {
324
+ "name": m.name,
325
+ "scope": m.scope,
326
+ "is_static": m.is_static,
327
+ "vi_path": m.vi_path,
328
+ }
329
+ for m in cls.methods
330
+ ],
331
+ }
332
+ print(json.dumps(data, indent=2))
333
+ else:
334
+ print(f"Class: {cls.name}")
335
+ if cls.parent_class:
336
+ print(f" Inherits: {cls.parent_class}")
337
+ if cls.private_data_ctl:
338
+ print(f" Private Data: {cls.private_data_ctl}")
339
+ if cls.methods:
340
+ print(" Methods:")
341
+ for m in cls.methods:
342
+ static = " [static]" if m.is_static else ""
343
+ print(f" - {m.name} ({m.scope}){static}")
344
+
345
+ elif input_path.suffix == ".lvlib":
346
+ # Single library
347
+ lib = parse_lvlib(input_path)
348
+ if args.json:
349
+ data = {
350
+ "name": lib.name,
351
+ "path": str(lib.path),
352
+ "version": lib.version,
353
+ "members": [
354
+ {"name": m.name, "type": m.member_type, "url": m.url}
355
+ for m in lib.members
356
+ ],
357
+ }
358
+ print(json.dumps(data, indent=2))
359
+ else:
360
+ print(f"Library: {lib.name}")
361
+ if lib.version:
362
+ print(f" Version: {lib.version}")
363
+ if lib.members:
364
+ print(f" Members ({len(lib.members)}):")
365
+ for m in lib.members:
366
+ print(f" - {m.name} [{m.member_type}]")
367
+
368
+ elif input_path.is_dir():
369
+ # Directory - discover full project
370
+ structure = discover_project_structure(input_path)
371
+
372
+ if args.plan:
373
+ plan = generate_python_structure_plan(structure)
374
+ print(plan)
375
+ elif args.json:
376
+ print(json.dumps(structure, indent=2))
377
+ else:
378
+ print(f"Project Structure: {input_path}")
379
+ print(f" Libraries: {len(structure['libraries'])}")
380
+ print(f" Classes: {len(structure['classes'])}")
381
+ print(f" Standalone VIs: {len(structure['standalone_vis'])}")
382
+ print()
383
+ if structure['classes']:
384
+ print("Classes:")
385
+ for cls in structure['classes']:
386
+ methods = len(cls['methods'])
387
+ print(f" - {cls['name']} ({methods} methods)")
388
+
389
+ else:
390
+ print(f"Error: Unsupported file type: {input_path}", file=sys.stderr)
391
+ return 1
392
+
393
+ return 0
394
+
395
+ except Exception as e:
396
+ print(f"Error: {e}", file=sys.stderr)
397
+ return 1
398
+
399
+
400
+ def cmd_mcp(args: argparse.Namespace) -> int:
401
+ """Handle the mcp command - run MCP server."""
402
+ from .mcp.server import main as mcp_main
403
+
404
+ try:
405
+ print("Starting MCP server...", file=sys.stderr)
406
+ mcp_main()
407
+ return 0
408
+ except KeyboardInterrupt:
409
+ print("\nShutting down MCP server...", file=sys.stderr)
410
+ return 0
411
+ except Exception as e:
412
+ print(f"Error: {e}", file=sys.stderr)
413
+ return 1
414
+
415
+
416
+ def cmd_describe(args: argparse.Namespace) -> int:
417
+ """Handle the describe command - human-readable VI description."""
418
+ from .graph.describe import describe_vi
419
+
420
+ input_path = Path(args.input_path)
421
+ if not input_path.exists():
422
+ print(f"Error: Path not found: {input_path}", file=sys.stderr)
423
+ return 1
424
+
425
+ _configure_resolvers(args)
426
+
427
+ try:
428
+ graph = InMemoryVIGraph()
429
+ search_paths = [Path(p) for p in args.search_paths]
430
+ graph.load_vi(str(input_path), search_paths=search_paths)
431
+
432
+ vi_name = graph.resolve_vi_name(input_path.name)
433
+
434
+ print(describe_vi(graph, vi_name))
435
+
436
+ if args.chart:
437
+ from .graph.flowchart import flowchart
438
+
439
+ print()
440
+ print("## Dataflow Chart")
441
+ print()
442
+ print("```mermaid")
443
+ print(flowchart(graph, vi_name))
444
+ print("```")
445
+
446
+ return 0
447
+ except (ValueError, FileNotFoundError, KeyError) as e:
448
+ print(f"Error: {e}", file=sys.stderr)
449
+ return 1
450
+
451
+
452
+ def cmd_init(args: argparse.Namespace) -> int:
453
+ """Handle the init command — create a project-local .lvkit/ store."""
454
+ root = Path(args.directory).resolve()
455
+ if not root.is_dir():
456
+ print(f"Error: Not a directory: {root}", file=sys.stderr)
457
+ return 1
458
+
459
+ store = init_project_store(root)
460
+ print(f"Initialized project store at {store}")
461
+ print(f" README: {store / 'README.md'}")
462
+
463
+ # Optional: install LLM editor skills
464
+ skills = getattr(args, "skills", None)
465
+ force = getattr(args, "force", False)
466
+ if skills in ("claude", "all"):
467
+ try:
468
+ written = install_claude_skills(root, force=force)
469
+ except FileExistsError as e:
470
+ print(f"Error: {e}", file=sys.stderr)
471
+ return 1
472
+ if written:
473
+ print(f"Installed {len(written)} Claude Code skill(s):")
474
+ for p in written:
475
+ print(f" {p}")
476
+ else:
477
+ print("Claude Code skills already up to date.")
478
+ if skills in ("copilot", "all"):
479
+ try:
480
+ copilot_written = install_copilot_skills(root, force=force)
481
+ except FileExistsError as e:
482
+ print(f"Error: {e}", file=sys.stderr)
483
+ return 1
484
+ if copilot_written:
485
+ print(f"Installed {len(copilot_written)} Copilot file(s):")
486
+ for p in copilot_written:
487
+ print(f" {p}")
488
+ else:
489
+ print("Copilot files already up to date.")
490
+
491
+ print()
492
+ print("Next steps:")
493
+ print(
494
+ " - Create .lvkit/primitives.json to override primitive mappings"
495
+ " (use lvkit's bundled primitives.json as a reference)"
496
+ )
497
+ print(
498
+ " - Add vi.lib mappings to .lvkit/vilib/<category>.json and register them"
499
+ " in .lvkit/vilib/_index.json"
500
+ )
501
+ print(" - lvkit will check .lvkit/ before its bundled data when resolving.")
502
+ if not skills:
503
+ print(
504
+ " - Run `lvkit init --skills all` to install resolve workflows"
505
+ " into Claude Code and/or Copilot."
506
+ )
507
+ return 0
508
+
509
+
510
+ def cmd_diff(args: argparse.Namespace) -> int:
511
+ """Handle the diff command — compare two VI versions."""
512
+ from .graph.diff import diff_structured, diff_text
513
+
514
+ path_a = Path(args.vi_a)
515
+ path_b = Path(args.vi_b)
516
+
517
+ for p in (path_a, path_b):
518
+ if not p.exists():
519
+ print(f"Error: Path not found: {p}", file=sys.stderr)
520
+ return 1
521
+
522
+ _configure_resolvers(args)
523
+ search_paths = [Path(p) for p in args.search_paths]
524
+
525
+ try:
526
+ graph_a = InMemoryVIGraph()
527
+ graph_a.load_vi(str(path_a), search_paths=search_paths)
528
+ vi_name_a = graph_a.resolve_vi_name(path_a.name)
529
+
530
+ graph_b = InMemoryVIGraph()
531
+ graph_b.load_vi(str(path_b), search_paths=search_paths)
532
+ vi_name_b = graph_b.resolve_vi_name(path_b.name)
533
+
534
+ if args.long:
535
+ report = diff_structured(graph_a, graph_b, vi_name_a, vi_name_b)
536
+ if report.is_empty():
537
+ print("No changes detected.")
538
+ else:
539
+ print(report.format())
540
+ else:
541
+ result = diff_text(
542
+ graph_a, graph_b, vi_name_a, vi_name_b,
543
+ label_a=str(path_a), label_b=str(path_b),
544
+ )
545
+ if result:
546
+ print(result)
547
+ else:
548
+ print("No changes detected.")
549
+
550
+ return 0
551
+ except (ValueError, FileNotFoundError, KeyError) as e:
552
+ print(f"Error: {e}", file=sys.stderr)
553
+ return 1
554
+
555
+
556
+ def cmd_generate(args: argparse.Namespace) -> int:
557
+ """Handle the generate command - AST-based Python generation."""
558
+ from .pipeline import generate_python
559
+
560
+ input_path = Path(args.input_path)
561
+
562
+ if not input_path.exists():
563
+ print(f"Error: Path not found: {input_path}", file=sys.stderr)
564
+ return 1
565
+
566
+ _configure_resolvers(args)
567
+
568
+ try:
569
+ sp = [Path(p) for p in args.search_paths] if args.search_paths else None
570
+ result = generate_python(
571
+ input_path,
572
+ args.output,
573
+ search_paths=sp,
574
+ expand_subvis=not args.no_expand,
575
+ soft_unresolved=args.placeholder_on_unresolved,
576
+ )
577
+ return 1 if result["error"] > 0 else 0
578
+
579
+ except (ValueError, FileNotFoundError, KeyError, NotImplementedError) as e:
580
+ print(f"Error: {e}", file=sys.stderr)
581
+ traceback.print_exc()
582
+ return 1
583
+
584
+
585
+ def cmd_docs(args: argparse.Namespace) -> int:
586
+ """Handle the docs command - generate HTML documentation."""
587
+ from .docs.generate import generate_documents
588
+
589
+ input_path = Path(args.input_path)
590
+
591
+ if not input_path.exists():
592
+ print(f"Error: Path not found: {input_path}", file=sys.stderr)
593
+ return 1
594
+
595
+ _configure_resolvers(args)
596
+
597
+ try:
598
+ result = generate_documents(
599
+ library_path=str(input_path),
600
+ output_dir=args.output_dir,
601
+ search_paths=args.search_paths if args.search_paths else None,
602
+ expand_subvis=not args.no_expand,
603
+ )
604
+ print("\n" + result)
605
+ return 0
606
+
607
+ except Exception as e:
608
+ print(f"Error: {e}", file=sys.stderr)
609
+ traceback.print_exc()
610
+ return 1
611
+
612
+
613
+ def cmd_visualize(args: argparse.Namespace) -> int:
614
+ """Handle the visualize command — interactive graph in browser."""
615
+ input_path = Path(args.input_path)
616
+ if not input_path.exists():
617
+ print(f"Error: Path not found: {input_path}", file=sys.stderr)
618
+ return 1
619
+
620
+ _configure_resolvers(args)
621
+
622
+ graph = InMemoryVIGraph()
623
+ search_paths = (
624
+ [Path(p) for p in args.search_paths] if args.search_paths else None
625
+ )
626
+ expand = not args.no_expand
627
+
628
+ suffix = input_path.suffix.lower()
629
+ if suffix == ".lvclass":
630
+ graph.load_lvclass(str(input_path), expand, search_paths)
631
+ elif suffix == ".lvlib":
632
+ graph.load_lvlib(str(input_path), expand, search_paths)
633
+ elif input_path.is_dir():
634
+ graph.load_directory(str(input_path), expand, search_paths)
635
+ else:
636
+ graph.load_vi(str(input_path), expand, search_paths)
637
+
638
+ output = Path(args.output)
639
+
640
+ # Default format: flowchart for dataflow, interactive for deps
641
+ fmt = args.format or ("interactive" if args.mode == "deps" else "flowchart")
642
+
643
+ if fmt == "flowchart":
644
+ from .graph.flowchart import flowchart_html
645
+
646
+ vis = list(graph.list_vis())
647
+ primary_vi = vis[0] if vis else ""
648
+ html = flowchart_html(graph, primary_vi)
649
+ output.parent.mkdir(parents=True, exist_ok=True)
650
+ output.write_text(html)
651
+ else:
652
+ try:
653
+ import pyvis # type: ignore[import-untyped] # noqa: F401
654
+ except ImportError:
655
+ print(
656
+ "Error: pyvis not installed. Run: pip install pyvis",
657
+ file=sys.stderr,
658
+ )
659
+ return 1
660
+ if args.mode == "deps":
661
+ _visualize_deps(graph, output)
662
+ else:
663
+ _visualize_dataflow(graph, output)
664
+
665
+ print(f"Graph saved to {args.output}")
666
+
667
+ if args.open:
668
+ import webbrowser
669
+ webbrowser.open(f"file://{Path(args.output).resolve()}")
670
+
671
+ return 0
672
+
673
+
674
+ _GRAPH_OPTIONS = """
675
+ {
676
+ "physics": {
677
+ "barnesHut": {
678
+ "gravitationalConstant": -8000,
679
+ "centralGravity": 0.1,
680
+ "springLength": 200,
681
+ "springConstant": 0.04,
682
+ "damping": 0.3
683
+ }
684
+ },
685
+ "edges": {
686
+ "arrows": {
687
+ "to": {"enabled": true, "scaleFactor": 1.0, "type": "arrow"}
688
+ },
689
+ "color": {"color": "#555", "highlight": "#000"},
690
+ "width": 2,
691
+ "smooth": {"type": "curvedCW", "roundness": 0.15}
692
+ },
693
+ "nodes": {
694
+ "font": {"size": 14, "face": "arial", "bold": {"face": "arial"}},
695
+ "borderWidth": 2,
696
+ "shadow": true
697
+ },
698
+ "interaction": {
699
+ "hover": true,
700
+ "tooltipDelay": 100
701
+ }
702
+ }
703
+ """
704
+
705
+ _PROPERTIES_PANEL = """
706
+ <div id="props" style="position:fixed;top:10px;left:10px;width:320px;
707
+ background:white;border:1px solid #ccc;padding:12px;
708
+ border-radius:8px;font-family:monospace;font-size:12px;
709
+ z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.15);
710
+ max-height:80vh;overflow-y:auto">
711
+ <b style="font-size:14px">Properties</b>
712
+ <div id="propContent" style="margin-top:8px;color:#666">
713
+ Click a node to see details
714
+ </div>
715
+ </div>
716
+ <script>
717
+ network.on("click", function(params) {
718
+ if (params.nodes.length > 0) {
719
+ var nodeId = params.nodes[0];
720
+ var node = nodes.get(nodeId);
721
+ var html = "<b>" + (node.label || nodeId) + "</b><br><br>";
722
+ if (node.title) {
723
+ html += node.title.replace(/\\n/g, "<br>");
724
+ }
725
+ document.getElementById("propContent").innerHTML = html;
726
+ } else {
727
+ document.getElementById("propContent").innerHTML =
728
+ "Click a node to see details";
729
+ }
730
+ });
731
+ </script>
732
+ """
733
+
734
+
735
+ def _build_legend(mode: str) -> str:
736
+ """Build legend HTML for the graph."""
737
+ if mode == "deps":
738
+ return """
739
+ <div style="position:fixed;top:10px;right:10px;background:white;
740
+ border:1px solid #ccc;padding:12px;border-radius:8px;
741
+ font-family:monospace;font-size:13px;z-index:1000;
742
+ box-shadow:0 2px 8px rgba(0,0,0,0.15)">
743
+ <b style="font-size:14px">Dependency Graph</b><br><br>
744
+ <span style="color:#4CAF50">■</span> VI<br>
745
+ <span style="color:#FF9800">■</span> Library<br>
746
+ <span style="color:#2196F3">■</span> Class<br>
747
+ <span style="color:#9C27B0">■</span> Typedef<br>
748
+ <span style="color:#999">■</span> Stub (missing)<br>
749
+ <br><span style="color:#888">→ depends on</span>
750
+ </div>
751
+ """
752
+ return """
753
+ <div style="position:fixed;top:10px;right:10px;background:white;
754
+ border:1px solid #ccc;padding:12px;border-radius:8px;
755
+ font-family:monospace;font-size:13px;z-index:1000;
756
+ box-shadow:0 2px 8px rgba(0,0,0,0.15)">
757
+ <b style="font-size:14px">Dataflow Graph</b><br><br>
758
+ <span style="color:#4CAF50">■</span> SubVI call<br>
759
+ <span style="color:#2196F3">■</span> Primitive operation<br>
760
+ <span style="color:#FF9800">◆</span> Structure (case/loop)<br>
761
+ <span style="color:#9C27B0">●</span> Constant<br>
762
+ <br><span style="color:#888">→ data flow</span>
763
+ </div>
764
+ """
765
+
766
+
767
+ def _inject_extras(output: Path, mode: str) -> None:
768
+ """Inject legend and properties panel into generated HTML."""
769
+ html = output.read_text()
770
+ extras = _build_legend(mode) + _PROPERTIES_PANEL
771
+ html = html.replace("</body>", extras + "</body>")
772
+ output.write_text(html)
773
+
774
+
775
+ def _visualize_dataflow(
776
+ graph: InMemoryVIGraph, output: Path,
777
+ ) -> None:
778
+ """Visualize the dataflow graph for a single VI."""
779
+ from pyvis.network import Network # type: ignore[import-untyped]
780
+
781
+ vis = list(graph.list_vis())
782
+ if not vis:
783
+ print("Error: No VIs loaded", file=sys.stderr)
784
+ return
785
+ primary_vi = vis[0]
786
+
787
+ net = Network(
788
+ height="800px", width="100%", directed=True, notebook=False,
789
+ )
790
+ net.set_options(_GRAPH_OPTIONS)
791
+
792
+ node_styles = {
793
+ "vi": {"color": "#4CAF50", "shape": "box"},
794
+ "primitive": {"color": "#2196F3", "shape": "box"},
795
+ "structure": {"color": "#FF9800", "shape": "diamond"},
796
+ "constant": {"color": "#9C27B0", "shape": "ellipse"},
797
+ }
798
+
799
+ for nid in graph._vi_nodes.get(primary_vi, set()):
800
+ gnode = graph._graph.nodes[nid].get("node")
801
+ if not gnode or nid == primary_vi:
802
+ continue
803
+
804
+ kind = getattr(gnode, "kind", "unknown")
805
+ style = node_styles.get(kind, {"color": "#666", "shape": "box"})
806
+ label = _dataflow_label(gnode, kind)
807
+ tooltip = _dataflow_tooltip(gnode, kind, nid)
808
+
809
+ # Group by parent structure + frame for visual clustering
810
+ group = None
811
+ if gnode.parent and gnode.frame is not None:
812
+ group = f"{gnode.parent}::{gnode.frame}"
813
+ elif gnode.parent:
814
+ group = gnode.parent
815
+
816
+ net.add_node(
817
+ nid, label=label,
818
+ color=style["color"],
819
+ shape=style.get("shape", "box"),
820
+ title=tooltip,
821
+ group=group,
822
+ )
823
+
824
+ added = {n["id"] for n in net.nodes}
825
+ for nid in added:
826
+ for _, dest, _, data in graph._graph.out_edges(
827
+ nid, data=True, keys=True,
828
+ ):
829
+ if dest not in added:
830
+ continue
831
+ src_end = data.get("source")
832
+ dst_end = data.get("dest")
833
+ title = ""
834
+ if src_end and dst_end:
835
+ sn = src_end.name or ""
836
+ dn = dst_end.name or ""
837
+ if sn or dn:
838
+ title = f"{sn} → {dn}"
839
+ net.add_edge(nid, dest, title=title)
840
+
841
+ output.parent.mkdir(parents=True, exist_ok=True)
842
+ net.save_graph(str(output))
843
+ _inject_extras(output, "dataflow")
844
+
845
+
846
+ def _visualize_deps(
847
+ graph: InMemoryVIGraph, output: Path,
848
+ ) -> None:
849
+ """Visualize the dependency graph across VIs."""
850
+ from pyvis.network import Network # type: ignore[import-untyped]
851
+
852
+ net = Network(
853
+ height="800px", width="100%", directed=True, notebook=False,
854
+ )
855
+ net.set_options(_GRAPH_OPTIONS)
856
+
857
+ dep = graph._dep_graph
858
+ stubs = graph._stubs
859
+
860
+ for node_id in dep.nodes:
861
+ attrs = dep.nodes[node_id]
862
+ node_type = attrs.get("node_type", "vi")
863
+ is_stub = node_id in stubs
864
+
865
+ colors = {
866
+ "vi": "#4CAF50",
867
+ "library": "#FF9800",
868
+ "class": "#2196F3",
869
+ "typedef": "#9C27B0",
870
+ }
871
+ color = "#999" if is_stub else colors.get(node_type, "#666")
872
+
873
+ label = node_id.split(":")[-1] if ":" in node_id else node_id
874
+ tooltip = f"{node_type}: {node_id}"
875
+ if is_stub:
876
+ tooltip += "\n(missing/stub)"
877
+ fields = attrs.get("fields")
878
+ if fields:
879
+ tooltip += f"\nFields: {len(fields)}"
880
+ for i, f in enumerate(fields):
881
+ tooltip += f"\n [{i}] {f.name}"
882
+
883
+ net.add_node(
884
+ node_id, label=label, color=color,
885
+ shape="box",
886
+ title=tooltip,
887
+ borderWidth=1 if is_stub else 2,
888
+ font={"color": "#999"} if is_stub else {},
889
+ )
890
+
891
+ for src, dest in dep.edges:
892
+ net.add_edge(src, dest)
893
+
894
+ output.parent.mkdir(parents=True, exist_ok=True)
895
+ net.save_graph(str(output))
896
+ _inject_extras(output, "deps")
897
+
898
+
899
+ def _dataflow_label(gnode, kind: str) -> str:
900
+ """Build readable label for a dataflow node."""
901
+ name = gnode.name or ""
902
+ if kind == "constant":
903
+ val = getattr(gnode, "value", "")
904
+ return f"{val}" if val is not None else "const"
905
+ if kind == "structure":
906
+ lt = getattr(gnode, "loop_type", None)
907
+ frames = getattr(gnode, "frames", [])
908
+ if lt:
909
+ return "While Loop" if lt == "whileLoop" else "For Loop"
910
+ if frames:
911
+ return f"Case [{len(frames)} frames]"
912
+ return name or "Structure"
913
+ return name.replace(".vi", "") or "?"
914
+
915
+
916
+ def _dataflow_tooltip(gnode, kind: str, nid: str) -> str:
917
+ """Build detailed tooltip for a dataflow node."""
918
+ name = gnode.name or nid.split("::")[-1]
919
+ lines = [f"<b>{kind}: {name}</b>", f"ID: {nid}"]
920
+
921
+ prim_id = getattr(gnode, "prim_id", None)
922
+ if prim_id:
923
+ lines.append(f"primResID: {prim_id}")
924
+
925
+ node_type = getattr(gnode, "node_type", None)
926
+ if node_type:
927
+ lines.append(f"XML class: {node_type}")
928
+
929
+ terminals = getattr(gnode, "terminals", [])
930
+ inputs = [
931
+ t for t in terminals
932
+ if t.direction == "input" and not t.is_error_cluster
933
+ ]
934
+ outputs = [
935
+ t for t in terminals
936
+ if t.direction == "output" and not t.is_error_cluster
937
+ ]
938
+
939
+ if inputs:
940
+ lines.append("")
941
+ lines.append("<b>Inputs:</b>")
942
+ for t in inputs:
943
+ tname = t.name or f"idx{t.index}"
944
+ ttype = t.python_type()
945
+ lines.append(f" [{t.index}] {tname}: {ttype}")
946
+
947
+ if outputs:
948
+ lines.append("")
949
+ lines.append("<b>Outputs:</b>")
950
+ for t in outputs:
951
+ tname = t.name or f"idx{t.index}"
952
+ ttype = t.python_type()
953
+ lines.append(f" [{t.index}] {tname}: {ttype}")
954
+
955
+ if kind == "constant":
956
+ val = getattr(gnode, "value", None)
957
+ raw = getattr(gnode, "raw_value", None)
958
+ lv_type = getattr(gnode, "lv_type", None)
959
+ lines.append(f"\\nValue: {val!r}")
960
+ if raw:
961
+ lines.append(f"Raw: {raw}")
962
+ if lv_type:
963
+ lines.append(f"Type: {lv_type.to_python()}")
964
+
965
+ if kind == "structure":
966
+ frames = getattr(gnode, "frames", [])
967
+ if frames:
968
+ lines.append("")
969
+ lines.append("<b>Frames:</b>")
970
+ for f in frames:
971
+ default = " (default)" if f.is_default else ""
972
+ lines.append(
973
+ f" {f.selector_value}{default}"
974
+ )
975
+
976
+ return "\\n".join(lines)
977
+
978
+
979
+ if __name__ == "__main__":
980
+ sys.exit(main())