viur-cli 3.0.0b2__tar.gz → 3.0.1__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 (30) hide show
  1. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/LICENSE +1 -1
  2. {viur_cli-3.0.0b2/src/viur_cli.egg-info → viur_cli-3.0.1}/PKG-INFO +2 -2
  3. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/README.md +1 -1
  4. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/conf.py +1 -0
  5. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/local.py +59 -15
  6. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/scriptor/cli.py +85 -21
  7. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/scriptor/login.py +0 -4
  8. viur_cli-3.0.1/src/viur_cli/version.py +2 -0
  9. {viur_cli-3.0.0b2 → viur_cli-3.0.1/src/viur_cli.egg-info}/PKG-INFO +2 -2
  10. viur_cli-3.0.0b2/src/viur_cli/version.py +0 -2
  11. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/pyproject.toml +0 -0
  12. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/setup.cfg +0 -0
  13. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/__init__.py +0 -0
  14. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/_docs_cli.py +0 -0
  15. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/build.py +0 -0
  16. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/cli.py +0 -0
  17. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/cloud.py +0 -0
  18. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/deprecated.py +0 -0
  19. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/package.py +0 -0
  20. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/scriptor/__init__.py +0 -0
  21. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/scripts/__init__.py +0 -0
  22. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/scripts/get_pyodide.py +0 -0
  23. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/setup.py +0 -0
  24. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/update.py +0 -0
  25. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli/utils.py +0 -0
  26. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli.egg-info/SOURCES.txt +0 -0
  27. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli.egg-info/dependency_links.txt +0 -0
  28. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli.egg-info/entry_points.txt +0 -0
  29. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli.egg-info/requires.txt +0 -0
  30. {viur_cli-3.0.0b2 → viur_cli-3.0.1}/src/viur_cli.egg-info/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- Copyright © 2025 by Mausbrand Informationssysteme GmbH.
1
+ Copyright © 2026 by Mausbrand Informationssysteme GmbH.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viur-cli
3
- Version: 3.0.0b2
3
+ Version: 3.0.1
4
4
  Summary: Command-line interface for ViUR application maintenance.
5
5
  Author-email: "Andreas H. Kelch" <ak@mausbrand.de>
6
6
  License-Expression: MIT
@@ -346,7 +346,7 @@ viur-cli depends on
346
346
 
347
347
  ## License
348
348
 
349
- Copyright © 2025 by Mausbrand Informationssysteme GmbH.<br>
349
+ Copyright © 2026 by Mausbrand Informationssysteme GmbH.<br>
350
350
  Mausbrand and ViUR are registered trademarks of Mausbrand Informationssysteme GmbH.
351
351
 
352
352
  This project is free software under the MIT license.<br>
@@ -323,7 +323,7 @@ viur-cli depends on
323
323
 
324
324
  ## License
325
325
 
326
- Copyright © 2025 by Mausbrand Informationssysteme GmbH.<br>
326
+ Copyright © 2026 by Mausbrand Informationssysteme GmbH.<br>
327
327
  Mausbrand and ViUR are registered trademarks of Mausbrand Informationssysteme GmbH.
328
328
 
329
329
  This project is free software under the MIT license.<br>
@@ -269,6 +269,7 @@ class ScriptorConfig(Config):
269
269
  self.update({
270
270
  "base_url": self.DEFAULT_BASE_URL,
271
271
  "working_dir": self.DEFAULT_WORKING_DIR,
272
+ "cookies": {},
272
273
  })
273
274
  super().__init__(**kwargs)
274
275
 
@@ -1,4 +1,9 @@
1
+ import contextlib
2
+ import itertools
1
3
  import json
4
+ import sys
5
+ import threading
6
+ import time
2
7
 
3
8
  import click
4
9
  import requests
@@ -186,6 +191,41 @@ def check(dev):
186
191
  utils.echo_info("\U00002714 No vulnerabilities found.")
187
192
 
188
193
 
194
+ @contextlib.contextmanager
195
+ def _spinner(message):
196
+ """Show an animated spinner next to ``message`` until the block exits.
197
+
198
+ No-op (prints the message once) when stdout is not a TTY so CI logs
199
+ stay clean.
200
+ """
201
+ styled = click.style(message, fg="cyan")
202
+ if not sys.stdout.isatty():
203
+ click.echo(styled)
204
+ yield
205
+ return
206
+
207
+ done = threading.Event()
208
+
209
+ def spin():
210
+ for ch in itertools.cycle("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"):
211
+ if done.is_set():
212
+ break
213
+ sys.stdout.write(f"\r{styled} {click.style(ch, fg='cyan')}")
214
+ sys.stdout.flush()
215
+ time.sleep(0.1)
216
+
217
+ t = threading.Thread(target=spin, daemon=True)
218
+ t.start()
219
+ try:
220
+ yield
221
+ finally:
222
+ done.set()
223
+ t.join()
224
+ # Leave the message in place; swap the spinner glyph for a check mark.
225
+ sys.stdout.write(f"\r{styled} {click.style('✓', fg='cyan')}\n")
226
+ sys.stdout.flush()
227
+
228
+
189
229
  def do_checks(dev=True):
190
230
  """Run Python (pip-audit) and npm (npm audit) vulnerability scans.
191
231
 
@@ -204,11 +244,12 @@ def do_checks(dev=True):
204
244
 
205
245
  # --- Python vulnerability check via pip-audit (PyPA tool, OSV data) ---
206
246
  try:
207
- result = subprocess.run(
208
- ["uvx", "pip-audit", "--format", "json", "--vulnerability-service", "osv"],
209
- capture_output=True,
210
- text=True,
211
- )
247
+ with _spinner("Scanning Python dependencies (pip-audit)"):
248
+ result = subprocess.run(
249
+ ["uvx", "pip-audit", "--format", "json", "--vulnerability-service", "osv"],
250
+ capture_output=True,
251
+ text=True,
252
+ )
212
253
  except FileNotFoundError:
213
254
  echo_warning("uvx not found; skipping Python vulnerability check.")
214
255
  else:
@@ -229,7 +270,7 @@ def do_checks(dev=True):
229
270
  vuln_deps = [d for d in deps if d.get("vulns")]
230
271
  total_vulns = sum(len(d["vulns"]) for d in vuln_deps)
231
272
 
232
- click.echo("\n" + "=" * 60)
273
+ click.echo("\n\n" + "=" * 60)
233
274
  click.echo("Python Security Scan Results (pip-audit, OSV)")
234
275
  click.echo("=" * 60)
235
276
  click.echo(f"Total Packages: {len(deps)}")
@@ -240,6 +281,7 @@ def do_checks(dev=True):
240
281
  if vuln_deps:
241
282
  has_vulnerabilities = True
242
283
  click.echo("Run `uvx pip-audit --vulnerability-service osv` for per-package detail.")
284
+ click.echo()
243
285
 
244
286
  # --- npm vulnerability check ---
245
287
  if shutil.which("npm"):
@@ -258,7 +300,7 @@ def do_checks(dev=True):
258
300
  npm_builds.append({"name": build_name, "path": audit_path})
259
301
 
260
302
  if not npm_builds:
261
- click.echo("\n" + "=" * 60)
303
+ click.echo("\n\n" + "=" * 60)
262
304
  click.echo("No npm builds found in config - skipping npm audit")
263
305
  click.echo("=" * 60)
264
306
  else:
@@ -267,12 +309,13 @@ def do_checks(dev=True):
267
309
  try:
268
310
  npm_audit_dir = build["path"]
269
311
 
270
- npm_result = subprocess.run(
271
- ["npm", "audit", "--json"],
272
- capture_output=True,
273
- text=True,
274
- cwd=npm_audit_dir,
275
- )
312
+ with _spinner(f"Scanning npm dependencies ({build['name']})"):
313
+ npm_result = subprocess.run(
314
+ ["npm", "audit", "--json"],
315
+ capture_output=True,
316
+ text=True,
317
+ cwd=npm_audit_dir,
318
+ )
276
319
 
277
320
  npm_data = json.loads(npm_result.stdout, strict=False)
278
321
 
@@ -288,7 +331,7 @@ def do_checks(dev=True):
288
331
  has_vulnerabilities = True
289
332
 
290
333
  # Display npm scan summary
291
- click.echo("\n" + "=" * 60)
334
+ click.echo("\n\n" + "=" * 60)
292
335
  click.echo(f"npm Security Scan Results - {build['name']}")
293
336
  click.echo("=" * 60)
294
337
  click.echo(f"Build Path: {npm_audit_dir}")
@@ -309,6 +352,7 @@ def do_checks(dev=True):
309
352
 
310
353
  if total_npm_vulns > 0:
311
354
  click.echo(f"Run `npm audit` in {build['path']} for per-package detail.")
355
+ click.echo()
312
356
 
313
357
  except FileNotFoundError:
314
358
  echo_warning(f"npm audit directory not found: {build['path']}")
@@ -321,7 +365,7 @@ def do_checks(dev=True):
321
365
  f"Unexpected error during npm security check for {build['name']}: {e}"
322
366
  )
323
367
  else:
324
- click.echo("\n" + "=" * 60)
368
+ click.echo("\n\n" + "=" * 60)
325
369
  click.echo("npm not found - skipping npm audit")
326
370
  click.echo("=" * 60)
327
371
 
@@ -4,6 +4,7 @@ import json
4
4
  import requests
5
5
  import os
6
6
  import hashlib
7
+ import difflib
7
8
  import asyncio
8
9
  import sys
9
10
  import glob
@@ -34,7 +35,7 @@ def script():
34
35
 
35
36
 
36
37
  @script.command()
37
- @click.option('--url', default=None, help='Set the url')
38
+ @click.option('--url', default=None, help='Set the server url')
38
39
  @click.option('--username', default=None, help='Set the username')
39
40
  @click.option('--working_dir', default=None, help='Set the working directory where scripts are stored to')
40
41
  def configure(url: str, username: str, working_dir: str):
@@ -43,15 +44,32 @@ def configure(url: str, username: str, working_dir: str):
43
44
  Only flags that are passed are written; the rest of the
44
45
  `viur_scriptor_config.json` keeps its previous values.
45
46
  """
47
+ if not any([url, username, working_dir]):
48
+ click.echo("No parameters provided. Use one or more of the following options:")
49
+ click.echo(" --url Set the server URL")
50
+ click.echo(" --username Set the username")
51
+ click.echo(" --working_dir Set the working directory where scripts are stored to")
52
+ return
53
+
54
+ changed = []
46
55
 
47
56
  if url:
48
57
  scriptor_config["base_url"] = url
58
+ changed.append(f"url = {url}")
49
59
 
50
60
  if username:
51
61
  scriptor_config["username"] = username
62
+ changed.append(f"username = {username}")
52
63
 
53
64
  if working_dir:
54
65
  scriptor_config["working_dir"] = working_dir.replace("\\", "/")
66
+ changed.append(f"working_dir = {working_dir}")
67
+
68
+ click.echo("Configuration updated:")
69
+ for entry in changed:
70
+ click.echo(f" {entry}")
71
+
72
+ scriptor_config.save()
55
73
 
56
74
 
57
75
  @script.command()
@@ -93,8 +111,7 @@ def setup():
93
111
  scriptor_config.save()
94
112
  click.echo("Setup done")
95
113
  else:
96
- if "cookies" in scriptor_config:
97
- del scriptor_config["cookies"]
114
+ scriptor_config["cookies"] = {}
98
115
  click.echo("Failed to login. Did you maybe entered the wrong password?")
99
116
  except KeyboardInterrupt:
100
117
  pass
@@ -104,9 +121,9 @@ def check_session(ctx: click.Context):
104
121
  base_url = scriptor_config.get("base_url")
105
122
 
106
123
  s = requests.Session()
107
- s.cookies = cookiejar_from_dict(scriptor_config.get("cookies", {}))
124
+ s.cookies = cookiejar_from_dict(scriptor_config["cookies"])
108
125
 
109
- response = s.get(base_url + "/vi/user/view/self", cookies=scriptor_config.get("cookies", {}))
126
+ response = s.get(base_url + "/vi/user/view/self", cookies=scriptor_config["cookies"])
110
127
  if not response.ok:
111
128
  click.echo("Invalid session, please run `viur script setup` again. okay ?")
112
129
  ctx.invoke(setup)
@@ -115,7 +132,8 @@ def check_session(ctx: click.Context):
115
132
  get_modules()
116
133
 
117
134
  @script.command()
118
- @click.option('--force', default=False, help='Force replace files from server in local working directory')
135
+ @click.option('--force', default=False, is_flag=True,
136
+ help='Overwrite local files without asking for confirmation')
119
137
  @click.pass_context
120
138
  def pull(ctx: click.Context, force: bool):
121
139
  """Download all server-side Scriptor scripts into the local working_dir.
@@ -131,36 +149,74 @@ def pull(ctx: click.Context, force: bool):
131
149
  tree = await modules.get_module("script")
132
150
  working_dir = scriptor_config.get("working_dir")
133
151
 
152
+ stats = {"new": 0, "updated": 0, "skipped": 0, "unchanged": 0, "dirs": 0}
153
+
134
154
  async def process_entry(entry: dict, is_node: bool):
135
155
  _path = os.path.join(working_dir, entry["path"].lstrip("/"))
136
156
 
137
157
  if is_node:
138
158
  if not os.path.exists(_path):
159
+ click.echo(click.style(f" mkdir {entry['path']}", fg="blue"))
139
160
  os.makedirs(_path)
161
+ stats["dirs"] += 1
140
162
  else:
163
+ click.echo(f" check {entry['path']}", nl=False)
164
+
141
165
  def create_file():
142
166
  with open(_path, "a+") as f:
143
- f.write(entry["script"])
144
-
145
- click.echo(f"Pull {_path}")
167
+ f.write(entry["script"] or "")
146
168
 
147
169
  if os.path.exists(_path):
148
170
  if force:
171
+ with open(_path, "r") as f:
172
+ changed = f.read().splitlines() != (entry["script"] or "").splitlines()
149
173
  os.remove(_path)
150
174
  create_file()
175
+ if changed:
176
+ click.echo(click.style(" [updated]", fg="yellow"))
177
+ stats["updated"] += 1
178
+ else:
179
+ click.echo(click.style(" [ok]", fg="green"))
180
+ stats["unchanged"] += 1
151
181
  else:
152
182
  with open(_path, "r") as f:
153
- if hashlib.sha256(entry["script"].encode()).digest() \
154
- != hashlib.sha256(f.read().encode()).digest():
155
- try:
156
- if click.confirm(f"There is a difference with {entry['path']}. Overwrite?"):
157
- os.remove(_path)
158
- create_file()
159
- except click.exceptions.Abort:
160
- click.echo("\nSkipping...")
161
-
183
+ local_content = f.read()
184
+ remote_content = entry["script"] or ""
185
+ diff = list(difflib.unified_diff(
186
+ remote_content.splitlines(),
187
+ local_content.splitlines(),
188
+ fromfile=f"server/{entry['path'].lstrip('/')}",
189
+ tofile=f"local/{entry['path'].lstrip('/')}",
190
+ lineterm="",
191
+ ))
192
+ if diff:
193
+ click.echo(click.style(" [diff]", fg="yellow"))
194
+ for line in diff:
195
+ if line.startswith("+++") or line.startswith("---"):
196
+ click.echo(click.style(line, bold=True))
197
+ elif line.startswith("@@"):
198
+ click.echo(click.style(line, fg="cyan"))
199
+ elif line.startswith("+"):
200
+ click.echo(click.style(line, fg="green"))
201
+ elif line.startswith("-"):
202
+ click.echo(click.style(line, fg="red"))
203
+ else:
204
+ click.echo(line)
205
+ if click.confirm(f"Overwrite local {entry['path']} with remote version?"):
206
+ os.remove(_path)
207
+ create_file()
208
+ stats["updated"] += 1
209
+ else:
210
+ stats["skipped"] += 1
211
+ else:
212
+ click.echo(click.style(" [ok]", fg="green"))
213
+ stats["unchanged"] += 1
162
214
  else:
215
+ click.echo(click.style(" [new]", fg="green"))
163
216
  create_file()
217
+ stats["new"] += 1
218
+
219
+ click.echo("Fetching scripts from server...")
164
220
 
165
221
  # Process nodes first
166
222
  async for node in tree.list(skel_type="node"):
@@ -170,6 +226,13 @@ def pull(ctx: click.Context, force: bool):
170
226
  async for leaf in tree.list(skel_type="leaf"):
171
227
  await process_entry(leaf, False)
172
228
 
229
+ click.echo("")
230
+ click.echo(click.style("Summary:", bold=True))
231
+ click.echo(f" new: {stats['new']}")
232
+ click.echo(f" updated: {stats['updated']}")
233
+ click.echo(f" skipped: {stats['skipped']}")
234
+ click.echo(f" unchanged: {stats['unchanged']}")
235
+
173
236
  asyncio.run(main())
174
237
 
175
238
 
@@ -232,7 +295,7 @@ def push(ctx: click.Context, force: bool, watch: bool):
232
295
  with open(_real_file, "r") as f:
233
296
  file_content = f.read()
234
297
 
235
- if hashlib.sha256(entry["script"].encode("utf-8")).digest() \
298
+ if hashlib.sha256((entry["script"] or "").encode("utf-8")).digest() \
236
299
  != hashlib.sha256(file_content.encode("utf-8")).digest():
237
300
  can_push = force
238
301
  if not can_push:
@@ -317,9 +380,10 @@ def push(ctx: click.Context, force: bool, watch: bool):
317
380
 
318
381
  def on_modified(event):
319
382
  try:
320
- # check for tmp file
321
383
  if event.src_path.endswith("~"):
322
384
  return
385
+ if not os.path.exists(event.src_path):
386
+ return
323
387
  if event.src_path not in modified_files:
324
388
  modified_files[event.src_path] = os.path.getmtime(event.src_path)
325
389
  elif os.path.getmtime(event.src_path) == modified_files[event.src_path]:
@@ -331,7 +395,7 @@ def push(ctx: click.Context, force: bool, watch: bool):
331
395
  print(f"Error: on file {event.src_path} {e}")
332
396
  traceback.print_exc()
333
397
 
334
- regexes = [r".*\.py"]
398
+ regexes = [r".*\.py$"]
335
399
  ignore_regexes = []
336
400
  ignore_directories = True
337
401
  case_sensitive = False
@@ -53,7 +53,3 @@ def ensure_login(
53
53
  scriptor_config.save()
54
54
 
55
55
  return True
56
-
57
-
58
-
59
-
@@ -0,0 +1,2 @@
1
+ __version__ = "3.0.1"
2
+ MINIMAL_UV = "0.9.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viur-cli
3
- Version: 3.0.0b2
3
+ Version: 3.0.1
4
4
  Summary: Command-line interface for ViUR application maintenance.
5
5
  Author-email: "Andreas H. Kelch" <ak@mausbrand.de>
6
6
  License-Expression: MIT
@@ -346,7 +346,7 @@ viur-cli depends on
346
346
 
347
347
  ## License
348
348
 
349
- Copyright © 2025 by Mausbrand Informationssysteme GmbH.<br>
349
+ Copyright © 2026 by Mausbrand Informationssysteme GmbH.<br>
350
350
  Mausbrand and ViUR are registered trademarks of Mausbrand Informationssysteme GmbH.
351
351
 
352
352
  This project is free software under the MIT license.<br>
@@ -1,2 +0,0 @@
1
- __version__ = "3.0.0.beta2"
2
- MINIMAL_UV = "0.9.15"
File without changes
File without changes
File without changes