vortex-cli 4.20.0__tar.gz → 4.21.0__tar.gz

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 (58) hide show
  1. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/PKG-INFO +48 -3
  2. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/README.md +47 -2
  3. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/pyproject.toml +10 -2
  4. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/cli.py +126 -5
  5. vortex_cli-4.21.0/vortex/commands/agent.py +27 -0
  6. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/clone.py +1 -1
  7. vortex_cli-4.21.0/vortex/commands/compile.py +208 -0
  8. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/db.py +8 -3
  9. vortex_cli-4.21.0/vortex/commands/docs.py +82 -0
  10. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/export.py +13 -9
  11. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/grep.py +5 -1
  12. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/import_.py +5 -0
  13. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/list.py +1 -0
  14. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/log.py +4 -4
  15. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/new.py +59 -23
  16. vortex_cli-4.21.0/vortex/commands/schema.py +394 -0
  17. vortex_cli-4.21.0/vortex/commands/use.py +65 -0
  18. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/watch.py +11 -4
  19. vortex_cli-4.21.0/vortex/docs/Blackbook v2.md +2753 -0
  20. vortex_cli-4.21.0/vortex/docs/index.html +341 -0
  21. vortex_cli-4.21.0/vortex/docs/marked.min.js +6 -0
  22. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/main.py +49 -2
  23. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/models.py +7 -5
  24. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/soap.py +151 -5
  25. vortex_cli-4.21.0/vortex/templates/agent/CLAUDE.md +44 -0
  26. vortex_cli-4.21.0/vortex/templates/agent/skills/puakma-database/SKILL.md +107 -0
  27. vortex_cli-4.21.0/vortex/templates/agent/skills/puakma-design-elements/SKILL.md +95 -0
  28. vortex_cli-4.21.0/vortex/templates/agent/skills/puakma-overview/SKILL.md +64 -0
  29. vortex_cli-4.21.0/vortex/templates/agent/skills/vortex-workflow/SKILL.md +103 -0
  30. vortex_cli-4.21.0/vortex/templates/agent/vortex.code-snippets +437 -0
  31. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/util.py +13 -13
  32. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/workspace.py +35 -1
  33. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/PKG-INFO +48 -3
  34. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/SOURCES.txt +12 -0
  35. vortex_cli-4.20.0/vortex/commands/docs.py +0 -44
  36. vortex_cli-4.20.0/vortex/commands/use.py +0 -28
  37. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/LICENSE +0 -0
  38. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/setup.cfg +0 -0
  39. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/__init__.py +0 -0
  40. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/__main__.py +0 -0
  41. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/colour.py +0 -0
  42. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/__init__.py +0 -0
  43. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/clean.py +0 -0
  44. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/code.py +0 -0
  45. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/config.py +0 -0
  46. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/copy.py +0 -0
  47. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/delete.py +0 -0
  48. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/execute.py +0 -0
  49. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/commands/find.py +0 -0
  50. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/constants.py +0 -0
  51. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/docs/Blackbook.pdf +0 -0
  52. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/lib/puakma-6.0.40.jar +0 -0
  53. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/logging.py +0 -0
  54. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex/spinner.py +0 -0
  55. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/dependency_links.txt +0 -0
  56. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/entry_points.txt +0 -0
  57. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/requires.txt +0 -0
  58. {vortex_cli-4.20.0 → vortex_cli-4.21.0}/vortex_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_cli
3
- Version: 4.20.0
3
+ Version: 4.21.0
4
4
  Summary: Vortex CLI
5
5
  Author-email: Jordan Amos <jordan.amos@gmail.com>
6
6
  License: MIT License
@@ -121,7 +121,10 @@ For a full list of commands see `--help`.
121
121
  - `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
122
122
  - `copy`: Copy a Design Object from one application to another.
123
123
  - `delete`: Delete Design Objects by ID.
124
- - `db`: Interact with Database Connections.
124
+ - `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
125
+ - `schema`: Manage the Puakma data dictionary (PMATABLE/ATTRIBUTE): record table/column definitions and print the DDL to run by hand. Never executes DDL.
126
+ - `compile` (or `build`): Compile an application's Java Design Objects into `zbin/` using `javac`.
127
+ - `agent`: Generate agent/editor support files (CLAUDE.md, Puakma skills, code snippets) in the workspace.
125
128
  - `docs`: Open the Tornado Server Blackbook.
126
129
  - `execute`: Execute a command on the server.
127
130
 
@@ -144,7 +147,7 @@ vortex new object
144
147
  # → pick app from locally cloned list
145
148
  # → pick design type
146
149
  # → enter name
147
- # → enter content type (resource only)
150
+ # → enter content type (resource/documentation only)
148
151
  # → enter parent page, open action, save action (page only)
149
152
  # → preview and confirm
150
153
 
@@ -163,3 +166,45 @@ Pass `--yes` (or `-y`) to skip Y/y confirmation prompts, useful for scripting:
163
166
  vortex --yes delete 123 456
164
167
  vortex -y new object --name MyAction --app-id 10 --type action
165
168
  ```
169
+
170
+ ### Compiling Java Design Objects
171
+
172
+ `vortex compile` (alias `build`) compiles an application's Java Design Objects into `zbin/`
173
+ with a real `javac`, independent of the IDE's incremental build - real compile errors, a
174
+ nonzero exit code on failure, and no stub class files. `javac` is found via `java_home` in the
175
+ server config, then `$JAVA_HOME`, then `PATH` (`--javac PATH` to override), and `--release` is
176
+ derived from the application's Java class version. It also warns when nested/inner/anonymous
177
+ classes are produced, since these cannot be deployed as Design Objects.
178
+
179
+ ```
180
+ vortex compile # compile all locally cloned apps for the server
181
+ vortex compile 13 # compile one app
182
+ vortex compile 13 --upload # also upload changed .class files
183
+ ```
184
+
185
+ While `vortex watch` is running, `--upload` isn't needed - watch uploads new class files from
186
+ `zbin/` automatically.
187
+
188
+ ### Managing the Data Dictionary
189
+
190
+ `vortex schema` records design-time table and column definitions in the Puakma data
191
+ dictionary (the PMATABLE/ATTRIBUTE tables, which also back `vortex db --schema`) and prints
192
+ the matching DDL for you to run by hand. It never executes DDL, and deletes only remove
193
+ dictionary rows - never real tables or columns.
194
+
195
+ ```
196
+ vortex schema mydb --add-table invoice --description "Customer invoices"
197
+ vortex schema mydb --add-column invoice invoice_id --type BIGINT --pk --auto-increment
198
+ vortex schema mydb --add-column invoice total --type NUMERIC --size 10,2 --not-null
199
+ vortex schema mydb --add-column invoice customer_id --type BIGINT --ref customer.customer_id
200
+ vortex schema mydb --ddl invoice # print CREATE TABLE from the dictionary
201
+ ```
202
+
203
+ ### Agent Support Files
204
+
205
+ `vortex agent` copies bundled support files into the workspace `.vscode` directory so they are
206
+ included in the generated code-workspace: a `CLAUDE.md` with Puakma ground rules for coding
207
+ agents, a `.claude/skills/` library covering Puakma development and the vortex workflow, and a
208
+ `vortex.code-snippets` file with common Puakma Java/HTML snippets. Existing files are never
209
+ overwritten, so they are safe to customise. These files are also generated automatically on
210
+ `vortex --init` and before `vortex code` opens the workspace.
@@ -79,7 +79,10 @@ For a full list of commands see `--help`.
79
79
  - `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
80
80
  - `copy`: Copy a Design Object from one application to another.
81
81
  - `delete`: Delete Design Objects by ID.
82
- - `db`: Interact with Database Connections.
82
+ - `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
83
+ - `schema`: Manage the Puakma data dictionary (PMATABLE/ATTRIBUTE): record table/column definitions and print the DDL to run by hand. Never executes DDL.
84
+ - `compile` (or `build`): Compile an application's Java Design Objects into `zbin/` using `javac`.
85
+ - `agent`: Generate agent/editor support files (CLAUDE.md, Puakma skills, code snippets) in the workspace.
83
86
  - `docs`: Open the Tornado Server Blackbook.
84
87
  - `execute`: Execute a command on the server.
85
88
 
@@ -102,7 +105,7 @@ vortex new object
102
105
  # → pick app from locally cloned list
103
106
  # → pick design type
104
107
  # → enter name
105
- # → enter content type (resource only)
108
+ # → enter content type (resource/documentation only)
106
109
  # → enter parent page, open action, save action (page only)
107
110
  # → preview and confirm
108
111
 
@@ -121,3 +124,45 @@ Pass `--yes` (or `-y`) to skip Y/y confirmation prompts, useful for scripting:
121
124
  vortex --yes delete 123 456
122
125
  vortex -y new object --name MyAction --app-id 10 --type action
123
126
  ```
127
+
128
+ ### Compiling Java Design Objects
129
+
130
+ `vortex compile` (alias `build`) compiles an application's Java Design Objects into `zbin/`
131
+ with a real `javac`, independent of the IDE's incremental build - real compile errors, a
132
+ nonzero exit code on failure, and no stub class files. `javac` is found via `java_home` in the
133
+ server config, then `$JAVA_HOME`, then `PATH` (`--javac PATH` to override), and `--release` is
134
+ derived from the application's Java class version. It also warns when nested/inner/anonymous
135
+ classes are produced, since these cannot be deployed as Design Objects.
136
+
137
+ ```
138
+ vortex compile # compile all locally cloned apps for the server
139
+ vortex compile 13 # compile one app
140
+ vortex compile 13 --upload # also upload changed .class files
141
+ ```
142
+
143
+ While `vortex watch` is running, `--upload` isn't needed - watch uploads new class files from
144
+ `zbin/` automatically.
145
+
146
+ ### Managing the Data Dictionary
147
+
148
+ `vortex schema` records design-time table and column definitions in the Puakma data
149
+ dictionary (the PMATABLE/ATTRIBUTE tables, which also back `vortex db --schema`) and prints
150
+ the matching DDL for you to run by hand. It never executes DDL, and deletes only remove
151
+ dictionary rows - never real tables or columns.
152
+
153
+ ```
154
+ vortex schema mydb --add-table invoice --description "Customer invoices"
155
+ vortex schema mydb --add-column invoice invoice_id --type BIGINT --pk --auto-increment
156
+ vortex schema mydb --add-column invoice total --type NUMERIC --size 10,2 --not-null
157
+ vortex schema mydb --add-column invoice customer_id --type BIGINT --ref customer.customer_id
158
+ vortex schema mydb --ddl invoice # print CREATE TABLE from the dictionary
159
+ ```
160
+
161
+ ### Agent Support Files
162
+
163
+ `vortex agent` copies bundled support files into the workspace `.vscode` directory so they are
164
+ included in the generated code-workspace: a `CLAUDE.md` with Puakma ground rules for coding
165
+ agents, a `.claude/skills/` library covering Puakma development and the vortex workflow, and a
166
+ `vortex.code-snippets` file with common Puakma Java/HTML snippets. Existing files are never
167
+ overwritten, so they are safe to customise. These files are also generated automatically on
168
+ `vortex --init` and before `vortex code` opens the workspace.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "vortex_cli"
8
- version = "4.20.0"
8
+ version = "4.21.0"
9
9
  description = "Vortex CLI"
10
10
  requires-python = ">=3.10"
11
11
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -32,7 +32,15 @@ classifiers = [
32
32
  vortex = "vortex.__main__:main"
33
33
 
34
34
  [tool.setuptools.package-data]
35
- vortex = ["lib/puakma-6.0.40.jar", "docs/Blackbook.pdf"]
35
+ vortex = [
36
+ "lib/puakma-6.0.40.jar",
37
+ "docs/Blackbook.pdf",
38
+ "docs/Blackbook v2.md",
39
+ "docs/index.html",
40
+ "docs/marked.min.js",
41
+ "templates/agent/*",
42
+ "templates/agent/skills/*/*",
43
+ ]
36
44
 
37
45
  [tool.mypy]
38
46
  check_untyped_defs = true
@@ -32,7 +32,7 @@ _EXECUTE_PARSER_DESCRIPTION = """\
32
32
  """
33
33
 
34
34
 
35
- def _check_int_in_range(val: str, min_: int = 0, max_: int | None = 50) -> int:
35
+ def _check_int_in_range(val: str, min_: int = 1, max_: int | None = 50) -> int:
36
36
  new_val = int(val)
37
37
  if new_val < min_ or (max_ is not None and new_val > max_):
38
38
  raise ArgumentTypeError(f"%s is not between {min_} and {max_}. " % val)
@@ -90,8 +90,9 @@ def validate_args(
90
90
  ) -> None:
91
91
  if args.command == "new":
92
92
  msg = None
93
- is_update_mode = bool(getattr(args, "update_ids", None))
94
- if args.subcommand == "object":
93
+ is_update_mode = getattr(args, "update_ids", None) is not None
94
+ if args.subcommand in ("object", "obj"):
95
+ update_missing_ids = is_update_mode and not args.update_ids
95
96
  missing_required_fields = not is_update_mode and not (
96
97
  args.name and args.app_id and args.design_type
97
98
  )
@@ -114,7 +115,9 @@ def validate_args(
114
115
  or args.parent_page
115
116
  or args.content_type
116
117
  )
117
- if missing_required_fields and not wizard_mode:
118
+ if update_missing_ids:
119
+ msg = "--update requires at least one Design Object ID"
120
+ elif missing_required_fields and not wizard_mode:
118
121
  msg = "--name, --app-id, and --type are required unless using --update"
119
122
  elif missing_content_type:
120
123
  msg = (
@@ -670,11 +673,129 @@ def add_db_parser(command_parser: _SubParsersAction[ArgumentParser]) -> Argument
670
673
  action="store_true",
671
674
  help="Set this flag when running queries for altering/updating the database",
672
675
  )
676
+ _add_server_option(db_parser)
673
677
  return db_parser
674
678
 
675
679
 
676
680
  def add_docs_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
677
- command_parser.add_parser("docs", help="Open the Tornado Server Blackbook")
681
+ docs_parser = command_parser.add_parser(
682
+ "docs", help="Open the Tornado Server Blackbook"
683
+ )
684
+ docs_parser.add_argument(
685
+ "--serve",
686
+ action="store_true",
687
+ help=(
688
+ "Serve the Blackbook v2 as a searchable local website "
689
+ "instead of opening the PDF"
690
+ ),
691
+ )
692
+ docs_parser.add_argument(
693
+ "--port",
694
+ type=int,
695
+ default=8477,
696
+ metavar="PORT",
697
+ help="The port to serve the docs on with --serve [%(default)s]",
698
+ )
699
+
700
+
701
+ def add_schema_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
702
+ schema_parser = command_parser.add_parser(
703
+ "schema",
704
+ help=(
705
+ "Manage the Puakma data dictionary (PMATABLE/ATTRIBUTE): record "
706
+ "design-time table/column definitions and print the DDL to run "
707
+ "by hand. Never executes DDL"
708
+ ),
709
+ )
710
+ schema_parser.add_argument(
711
+ "database", help="The database name whose dictionary to manage"
712
+ )
713
+ schema_mutex = schema_parser.add_mutually_exclusive_group(required=True)
714
+ schema_mutex.add_argument("--add-table", metavar="TABLE")
715
+ schema_mutex.add_argument(
716
+ "--delete-table",
717
+ metavar="TABLE",
718
+ help="Remove a table definition (does NOT drop the real table)",
719
+ )
720
+ schema_mutex.add_argument("--add-column", nargs=2, metavar=("TABLE", "COLUMN"))
721
+ schema_mutex.add_argument("--update-column", nargs=2, metavar=("TABLE", "COLUMN"))
722
+ schema_mutex.add_argument(
723
+ "--delete-column",
724
+ nargs=2,
725
+ metavar=("TABLE", "COLUMN"),
726
+ help="Remove a column definition (does NOT alter the real table)",
727
+ )
728
+ schema_mutex.add_argument(
729
+ "--ddl",
730
+ metavar="TABLE",
731
+ dest="ddl_table",
732
+ help="Print CREATE TABLE DDL generated from the dictionary definitions",
733
+ )
734
+ col_group = schema_parser.add_argument_group("Column Options")
735
+ col_group.add_argument("--type", dest="type_", metavar="SQL_TYPE")
736
+ col_group.add_argument("--size", metavar="N", help="e.g. 50 or '10,2'")
737
+ col_group.add_argument("--not-null", action="store_true")
738
+ col_group.add_argument("--pk", action="store_true")
739
+ col_group.add_argument("--auto-increment", action="store_true")
740
+ col_group.add_argument("--unique", action="store_true")
741
+ col_group.add_argument("--default", metavar="VALUE")
742
+ col_group.add_argument(
743
+ "--ref", metavar="TABLE.COLUMN", help="Foreign key reference"
744
+ )
745
+ col_group.add_argument("--description", metavar="TEXT")
746
+ col_group.add_argument("--position", type=int, metavar="N")
747
+ _add_server_option(schema_parser)
748
+
749
+
750
+ def add_compile_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
751
+ compile_parser = command_parser.add_parser(
752
+ "compile",
753
+ aliases=["build"],
754
+ help=(
755
+ "Compile an app's Java design elements into zbin/ with javac. "
756
+ "Real compile errors, no IDE error-stub classes"
757
+ ),
758
+ )
759
+ compile_parser.add_argument(
760
+ "app_ids",
761
+ nargs="*",
762
+ type=int,
763
+ metavar="APP_ID",
764
+ help=(
765
+ "App id(s) to compile. Default is all locally cloned apps " "for the server"
766
+ ),
767
+ )
768
+ compile_parser.add_argument(
769
+ "--upload",
770
+ "-u",
771
+ action="store_true",
772
+ help=(
773
+ "Upload changed .class files after a successful compile. "
774
+ "Not needed while 'vortex watch' is running - watch uploads "
775
+ "new class files automatically"
776
+ ),
777
+ )
778
+ compile_parser.add_argument(
779
+ "--javac",
780
+ metavar="PATH",
781
+ dest="javac_path",
782
+ help=(
783
+ "Path to javac. Default: <java_home>/bin/javac from the server "
784
+ "config, else $JAVA_HOME, else javac on PATH"
785
+ ),
786
+ )
787
+ _add_server_option(compile_parser)
788
+
789
+
790
+ def add_agent_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
791
+ command_parser.add_parser(
792
+ "agent",
793
+ help=(
794
+ "Generate agent/editor support files in the workspace "
795
+ "(CLAUDE.md, Puakma skills, code snippets). Existing files "
796
+ "are never overwritten"
797
+ ),
798
+ )
678
799
 
679
800
 
680
801
  def add_execute_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from vortex.workspace import Workspace
6
+
7
+ logger = logging.getLogger("vortex")
8
+
9
+
10
+ def agent(workspace: Workspace) -> int:
11
+ """
12
+ Generates the agent/editor support files (CLAUDE.md, Puakma skills,
13
+ code snippets) in the workspace .vscode directory. Existing files are
14
+ never overwritten.
15
+ """
16
+ written, skipped = workspace.init_agent_files()
17
+ for path in written:
18
+ logger.info(f"Created '{path}'")
19
+ for path in skipped:
20
+ logger.debug(f"Skipped existing '{path}'")
21
+ if not written:
22
+ logger.info(f"All {len(skipped)} agent file(s) already exist. Nothing to do.")
23
+ else:
24
+ logger.info(
25
+ f"Created {len(written)} file(s), kept {len(skipped)} existing file(s)."
26
+ )
27
+ return 0
@@ -77,7 +77,7 @@ def _parse_app_xml(
77
77
  server: PuakmaServer, app_xml: ET.Element, app_id: int
78
78
  ) -> tuple[PuakmaApplication, ET.Element]:
79
79
  app_ele = app_xml.find("puakmaApplication", namespaces=None)
80
- if not app_ele:
80
+ if app_ele is None:
81
81
  raise ValueError(f"Application [{app_id}] does not exist")
82
82
 
83
83
  db_connections: list[DatabaseConnection] = []
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import configparser
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from vortex.models import DesignObject
13
+ from vortex.models import DesignObjectAmbiguousError
14
+ from vortex.models import DesignObjectNotFound
15
+ from vortex.models import DesignType
16
+ from vortex.models import PuakmaApplication
17
+ from vortex.models import PuakmaServer
18
+ from vortex.workspace import Workspace
19
+
20
+ logger = logging.getLogger("vortex")
21
+
22
+
23
+ def _find_javac(
24
+ workspace: Workspace, server: PuakmaServer, override: str | None
25
+ ) -> str | None:
26
+ if override:
27
+ if Path(override).is_file():
28
+ return override
29
+ return shutil.which(override)
30
+ config = configparser.ConfigParser()
31
+ config.read(workspace.server_config_file)
32
+ java_home = config.get(server.name, "java_home", fallback=None) or os.getenv(
33
+ "JAVA_HOME"
34
+ )
35
+ if java_home:
36
+ exe = "javac.exe" if sys.platform == "win32" else "javac"
37
+ candidate = Path(java_home) / "bin" / exe
38
+ if candidate.is_file():
39
+ return str(candidate)
40
+ return shutil.which("javac")
41
+
42
+
43
+ def _build_classpath(workspace: Workspace, server: PuakmaServer, app_dir: Path) -> str:
44
+ import vortex
45
+
46
+ lib_dir = Path(vortex.__file__).parent / "lib"
47
+ entries = [str(jar) for jar in sorted(lib_dir.glob("*.jar"))]
48
+ entries.extend(str(jar) for jar in sorted((app_dir / "zlib").rglob("*.jar")))
49
+ config = configparser.ConfigParser()
50
+ config.read(workspace.server_config_file)
51
+ server_libs = config.get(server.name, "lib_path", fallback=None)
52
+ if server_libs:
53
+ entries.extend(p for p in server_libs.split(",") if p)
54
+ entries.append(str(app_dir / "zbin"))
55
+ return os.pathsep.join(entries)
56
+
57
+
58
+ def _compile_app(
59
+ workspace: Workspace,
60
+ server: PuakmaServer,
61
+ app: PuakmaApplication,
62
+ javac: str,
63
+ ) -> tuple[int, list[Path]]:
64
+ """
65
+ Compiles an app's Java design sources into zbin/ with javac.
66
+ Returns (exit_code, compiled_class_files).
67
+ """
68
+ app_dir = workspace.path / app.path
69
+ sources = [
70
+ str(f)
71
+ for dir_name in DesignType.source_dirs()
72
+ for f in sorted((app_dir / dir_name).rglob("*.java"))
73
+ ]
74
+ if not sources:
75
+ logger.info(f"{app}: no Java sources to compile")
76
+ return 0, []
77
+
78
+ zbin = app_dir / "zbin"
79
+ zbin.mkdir(exist_ok=True)
80
+
81
+ cmd = [javac, "-d", str(zbin), "-cp", _build_classpath(workspace, server, app_dir)]
82
+ if app.java_class_version:
83
+ # Class file major version N == Java release N - 44 (61 -> 17)
84
+ cmd.extend(["--release", str(app.java_class_version[0] - 44)])
85
+ cmd.extend(sources)
86
+
87
+ logger.info(f"Compiling {len(sources)} source file(s) for {app}...")
88
+ result = subprocess.run(cmd, capture_output=True, text=True)
89
+ if result.stdout.strip():
90
+ print(result.stdout, end="")
91
+ if result.returncode != 0:
92
+ print(result.stderr, file=sys.stderr, end="")
93
+ logger.error(f"{app}: compilation FAILED")
94
+ return 1, []
95
+ if result.stderr.strip():
96
+ # javac warnings
97
+ print(result.stderr, file=sys.stderr, end="")
98
+
99
+ compiled = sorted(zbin.rglob("*.class"))
100
+ nested = [c for c in compiled if "$" in c.name]
101
+ if nested:
102
+ names = ", ".join(sorted({c.name for c in nested}))
103
+ logger.warning(
104
+ f"{app}: nested/inner/anonymous classes compiled ({names}) - "
105
+ "these CANNOT be deployed as design elements; refactor to "
106
+ "top-level SHARED_CODE classes"
107
+ )
108
+ logger.info(f"{app}: compilation OK")
109
+ return 0, compiled
110
+
111
+
112
+ async def _aupload_classes(
113
+ workspace: Workspace,
114
+ server: PuakmaServer,
115
+ to_upload: list[tuple[PuakmaApplication, DesignObject]],
116
+ ) -> int:
117
+ ret = 0
118
+ async with server as s:
119
+ await s.server_designer.ainitiate_connection()
120
+ for _app, obj in to_upload:
121
+ ok = await obj.aupload(s.download_designer, upload_source=False)
122
+ ret |= 0 if ok else 1
123
+ for app in {app for app, _obj in to_upload}:
124
+ workspace.mkdir(app)
125
+ return ret
126
+
127
+
128
+ def _collect_uploads(
129
+ app: PuakmaApplication, class_files: list[Path]
130
+ ) -> list[tuple[PuakmaApplication, DesignObject]]:
131
+ """
132
+ Matches compiled top-level classes to design objects, skipping classes
133
+ whose bytes already match the manifest's design data.
134
+ """
135
+ to_upload = []
136
+ for class_file in class_files:
137
+ if "$" in class_file.name:
138
+ continue
139
+ try:
140
+ _, obj = app.lookup_design_obj(class_file.stem)
141
+ except DesignObjectNotFound:
142
+ continue
143
+ except DesignObjectAmbiguousError as e:
144
+ logger.warning(f"Not uploading '{class_file.name}': {e}")
145
+ continue
146
+ if not obj.design_type.is_java_type or obj.is_jar_library:
147
+ continue
148
+ class_bytes = class_file.read_bytes()
149
+ try:
150
+ unchanged = obj.design_data == class_bytes
151
+ except Exception:
152
+ unchanged = False
153
+ if unchanged:
154
+ logger.debug(f"{obj.name}: unchanged, skipping upload")
155
+ continue
156
+ obj.design_data = class_bytes
157
+ to_upload.append((app, obj))
158
+ return to_upload
159
+
160
+
161
+ def compile_(
162
+ workspace: Workspace,
163
+ server: PuakmaServer,
164
+ app_ids: list[int],
165
+ *,
166
+ upload: bool = False,
167
+ javac_path: str | None = None,
168
+ ) -> int:
169
+ javac = _find_javac(workspace, server, javac_path)
170
+ if not javac:
171
+ logger.error(
172
+ "javac not found. Set 'java_home' in the server config, "
173
+ "$JAVA_HOME, or pass --javac PATH"
174
+ )
175
+ return 1
176
+ logger.debug(f"Using javac: {javac}")
177
+
178
+ apps = workspace.listapps(server)
179
+ if app_ids:
180
+ apps = [app for app in apps if app.id in app_ids]
181
+ missing = set(app_ids) - {app.id for app in apps}
182
+ if missing:
183
+ logger.error(f"No locally cloned app(s) with id(s): {sorted(missing)}")
184
+ return 1
185
+ if not apps:
186
+ logger.error("No locally cloned applications to compile")
187
+ return 1
188
+
189
+ ret = 0
190
+ compiled_per_app: list[tuple[PuakmaApplication, list[Path]]] = []
191
+ for app in apps:
192
+ app_ret, class_files = _compile_app(workspace, server, app, javac)
193
+ ret |= app_ret
194
+ if app_ret == 0 and class_files:
195
+ compiled_per_app.append((app, class_files))
196
+
197
+ if upload and ret == 0:
198
+ to_upload: list[tuple[PuakmaApplication, DesignObject]] = []
199
+ for app, class_files in compiled_per_app:
200
+ to_upload.extend(_collect_uploads(app, class_files))
201
+ if to_upload:
202
+ logger.info(f"Uploading {len(to_upload)} compiled class(es)...")
203
+ ret |= asyncio.run(_aupload_classes(workspace, server, to_upload))
204
+ else:
205
+ logger.info("All compiled classes match the server. Nothing to upload.")
206
+ elif upload and ret != 0:
207
+ logger.warning("Skipping upload because compilation failed")
208
+ return ret
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
+ import re
5
6
  import subprocess
6
7
  import sys
7
8
  import tempfile
@@ -159,14 +160,18 @@ def db(
159
160
  logger.error(f"No cloned applications with DB Connection ID '{conn_id}'")
160
161
  return 1
161
162
  if sql and not update:
162
- sql = sql + f" LIMIT {limit_n_results}"
163
+ # Strip a trailing ';' and don't stack LIMITs so the appended
164
+ # clause can't create invalid SQL
165
+ sql = sql.strip().rstrip(";").rstrip()
166
+ if not re.search(r"\bLIMIT\b", sql, re.IGNORECASE):
167
+ sql = sql + f" LIMIT {limit_n_results}"
163
168
  else:
164
- db_name = str(database).lower()
169
+ db_name = str(database).lower().replace("'", "''")
165
170
  update = False
166
171
  truncate_cols = False
167
172
  conn_id = server.puakma_db_conn_id
168
173
  if schema_table is not None:
169
- sql = GET_SCHEMA_SQL % (db_name, schema_table)
174
+ sql = GET_SCHEMA_SQL % (db_name, schema_table.lower().replace("'", "''"))
170
175
  elif list_tables:
171
176
  sql = LIST_TABLES_SQL % db_name
172
177
 
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import http.server
5
+ import logging
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import webbrowser
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger("vortex")
15
+
16
+ DOCS_DIR = Path(os.path.dirname(__file__), "..", "docs").resolve()
17
+ DEFAULT_PORT = 8477
18
+
19
+
20
+ class _DocsRequestHandler(http.server.SimpleHTTPRequestHandler):
21
+ def log_message(self, fmt: str, *args: Any) -> None:
22
+ logger.debug(f"{self.address_string()} - {fmt % args}")
23
+
24
+
25
+ def _serve(port: int) -> int:
26
+ handler = functools.partial(_DocsRequestHandler, directory=str(DOCS_DIR))
27
+ try:
28
+ server = http.server.ThreadingHTTPServer(("127.0.0.1", port), handler)
29
+ except OSError as e:
30
+ logger.error(f"Unable to start the docs server on port {port}: {e}")
31
+ return 1
32
+
33
+ url = f"http://127.0.0.1:{port}/"
34
+ logger.info(f"Serving the Blackbook at {url} (Ctrl+C to stop)")
35
+ webbrowser.open(url)
36
+ try:
37
+ server.serve_forever()
38
+ except KeyboardInterrupt:
39
+ pass
40
+ finally:
41
+ server.server_close()
42
+ return 0
43
+
44
+
45
+ def _open_pdf() -> int:
46
+ system = platform.system()
47
+ doc_path = DOCS_DIR / "Blackbook.pdf"
48
+
49
+ cmd = None
50
+ shell = False
51
+
52
+ if system == "Windows":
53
+ cmd = "start"
54
+ shell = True
55
+ # MacOS
56
+ elif system == "Darwin":
57
+ cmd = shutil.which("open")
58
+ elif system == "Linux":
59
+ cmd = shutil.which("xdg-open")
60
+
61
+ if cmd is None:
62
+ logger.error("Unable to determine default program to open file.")
63
+ return 1
64
+
65
+ try:
66
+ subprocess.Popen(
67
+ [cmd, str(doc_path)],
68
+ shell=shell,
69
+ stdout=subprocess.DEVNULL,
70
+ stderr=subprocess.DEVNULL,
71
+ )
72
+ except FileNotFoundError:
73
+ logger.error(f"Command '{cmd}' not found")
74
+ return 1
75
+
76
+ return 0
77
+
78
+
79
+ def docs(serve: bool = False, port: int = DEFAULT_PORT) -> int:
80
+ if serve:
81
+ return _serve(port)
82
+ return _open_pdf()