zexus 1.7.1 → 1.7.2

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 (159) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  45. package/src/zexus/cli/main.py +300 -20
  46. package/src/zexus/cli/zpm.py +1 -1
  47. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  48. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/lexer.py +10 -5
  53. package/src/zexus/concurrency_system.py +79 -0
  54. package/src/zexus/config.py +54 -0
  55. package/src/zexus/crypto_bridge.py +244 -8
  56. package/src/zexus/dap/__init__.py +10 -0
  57. package/src/zexus/dap/__main__.py +4 -0
  58. package/src/zexus/dap/dap_server.py +391 -0
  59. package/src/zexus/dap/debug_engine.py +298 -0
  60. package/src/zexus/environment.py +10 -1
  61. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  62. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  63. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  64. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  65. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  66. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  67. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/bytecode_compiler.py +441 -37
  70. package/src/zexus/evaluator/core.py +560 -49
  71. package/src/zexus/evaluator/expressions.py +122 -49
  72. package/src/zexus/evaluator/functions.py +417 -16
  73. package/src/zexus/evaluator/statements.py +521 -118
  74. package/src/zexus/evaluator/unified_execution.py +573 -72
  75. package/src/zexus/evaluator/utils.py +14 -2
  76. package/src/zexus/event_loop.py +186 -0
  77. package/src/zexus/lexer.py +742 -486
  78. package/src/zexus/lsp/__init__.py +1 -1
  79. package/src/zexus/lsp/definition_provider.py +163 -9
  80. package/src/zexus/lsp/server.py +22 -8
  81. package/src/zexus/lsp/symbol_provider.py +182 -9
  82. package/src/zexus/module_cache.py +237 -9
  83. package/src/zexus/object.py +64 -6
  84. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  85. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  86. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  87. package/src/zexus/parser/parser.py +786 -285
  88. package/src/zexus/parser/strategy_context.py +407 -66
  89. package/src/zexus/parser/strategy_structural.py +117 -19
  90. package/src/zexus/persistence.py +15 -1
  91. package/src/zexus/renderer/__init__.py +15 -0
  92. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  93. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  94. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  95. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  96. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  97. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  98. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  99. package/src/zexus/renderer/tk_backend.py +208 -0
  100. package/src/zexus/renderer/web_backend.py +260 -0
  101. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  103. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  104. package/src/zexus/runtime/file_flags.py +137 -0
  105. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  107. package/src/zexus/security.py +424 -34
  108. package/src/zexus/stdlib/fs.py +23 -18
  109. package/src/zexus/stdlib/http.py +289 -186
  110. package/src/zexus/stdlib/sockets.py +207 -163
  111. package/src/zexus/stdlib/websockets.py +282 -0
  112. package/src/zexus/stdlib_integration.py +369 -2
  113. package/src/zexus/strategy_recovery.py +6 -3
  114. package/src/zexus/type_checker.py +423 -0
  115. package/src/zexus/virtual_filesystem.py +189 -2
  116. package/src/zexus/vm/__init__.py +113 -3
  117. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  118. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  119. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  120. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  121. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  122. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  123. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  124. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  125. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  126. package/src/zexus/vm/async_optimizer.py +14 -1
  127. package/src/zexus/vm/binary_bytecode.py +659 -0
  128. package/src/zexus/vm/bytecode.py +28 -1
  129. package/src/zexus/vm/bytecode_converter.py +26 -12
  130. package/src/zexus/vm/cabi.c +1985 -0
  131. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  132. package/src/zexus/vm/cabi.h +127 -0
  133. package/src/zexus/vm/cache.py +557 -17
  134. package/src/zexus/vm/compiler.py +703 -5
  135. package/src/zexus/vm/fastops.c +15743 -0
  136. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  137. package/src/zexus/vm/fastops.pyx +288 -0
  138. package/src/zexus/vm/gas_metering.py +50 -9
  139. package/src/zexus/vm/jit.py +83 -2
  140. package/src/zexus/vm/native_jit_backend.py +1816 -0
  141. package/src/zexus/vm/native_runtime.cpp +1388 -0
  142. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  143. package/src/zexus/vm/optimizer.py +161 -11
  144. package/src/zexus/vm/parallel_vm.py +118 -42
  145. package/src/zexus/vm/peephole_optimizer.py +82 -4
  146. package/src/zexus/vm/profiler.py +38 -18
  147. package/src/zexus/vm/register_allocator.py +16 -5
  148. package/src/zexus/vm/register_vm.py +8 -5
  149. package/src/zexus/vm/vm.py +3411 -573
  150. package/src/zexus/vm/wasm_compiler.py +658 -0
  151. package/src/zexus/zexus_ast.py +63 -11
  152. package/src/zexus/zexus_token.py +13 -5
  153. package/src/zexus/zpm/installer.py +55 -15
  154. package/src/zexus/zpm/package_manager.py +1 -1
  155. package/src/zexus/zpm/registry.py +257 -28
  156. package/src/zexus.egg-info/PKG-INFO +7 -4
  157. package/src/zexus.egg-info/SOURCES.txt +116 -9
  158. package/src/zexus.egg-info/entry_points.txt +1 -0
  159. package/src/zexus.egg-info/requires.txt +4 -0
@@ -2,6 +2,10 @@
2
2
 
3
3
  # Base classes
4
4
  class Node:
5
+ # Source location for debugger / error reporting (set by parser)
6
+ line: int = 0
7
+ column: int = 0
8
+
5
9
  def __repr__(self):
6
10
  return f"{self.__class__.__name__}()"
7
11
 
@@ -33,13 +37,16 @@ class ConstStatement(Statement):
33
37
  """Const statement - immutable variable declaration
34
38
 
35
39
  const MAX_VALUE = 100;
40
+ const PI: float = 3.14;
36
41
  """
37
- def __init__(self, name, value):
42
+ def __init__(self, name, value, type_annotation=None):
38
43
  self.name = name
39
44
  self.value = value
45
+ self.type_annotation = type_annotation
40
46
 
41
47
  def __repr__(self):
42
- return f"ConstStatement(name={self.name}, value={self.value})"
48
+ type_str = f", type={self.type_annotation}" if self.type_annotation else ""
49
+ return f"ConstStatement(name={self.name}, value={self.value}{type_str})"
43
50
 
44
51
  class DataStatement(Statement):
45
52
  """Data statement - dataclass definition
@@ -386,13 +393,15 @@ class DebugStatement(Statement):
386
393
 
387
394
  # NEW: Try-catch statement
388
395
  class TryCatchStatement(Statement):
389
- def __init__(self, try_block, error_variable, catch_block):
396
+ def __init__(self, try_block, error_variable, catch_block, finally_block=None):
390
397
  self.try_block = try_block
391
398
  self.error_variable = error_variable
392
399
  self.catch_block = catch_block
400
+ self.finally_block = finally_block
393
401
 
394
402
  def __repr__(self):
395
- return f"TryCatchStatement(error_var={self.error_variable})"
403
+ has_finally = " +finally" if self.finally_block else ""
404
+ return f"TryCatchStatement(error_var={self.error_variable}{has_finally})"
396
405
 
397
406
  # NEW: External function declaration
398
407
  class ExternalDeclaration(Statement):
@@ -464,11 +473,13 @@ class TrailStatement(Statement):
464
473
 
465
474
  # Expression Nodes
466
475
  class Identifier(Expression):
467
- def __init__(self, value):
476
+ def __init__(self, value, type_annotation=None):
468
477
  self.value = value
478
+ self.type_annotation = type_annotation # Optional[str] for typed parameters
469
479
 
470
480
  def __repr__(self):
471
- return f"Identifier('{self.value}')"
481
+ type_str = f": {self.type_annotation}" if self.type_annotation else ""
482
+ return f"Identifier('{self.value}{type_str}')"
472
483
 
473
484
  def __str__(self):
474
485
  return self.value
@@ -497,6 +508,19 @@ class StringLiteral(Expression):
497
508
  def __str__(self):
498
509
  return self.value
499
510
 
511
+ class StringInterpolationExpression(Expression):
512
+ """String interpolation: "hello ${name}, you are ${age} years old"
513
+
514
+ parts is a list of (type, value) where type is "str" or "expr".
515
+ For "str" parts, value is a string literal.
516
+ For "expr" parts, value is a parsed Expression AST node.
517
+ """
518
+ def __init__(self, parts):
519
+ self.parts = parts # list of ("str", string) or ("expr", Expression)
520
+
521
+ def __repr__(self):
522
+ return f"StringInterpolation({len(self.parts)} parts)"
523
+
500
524
  class Boolean(Expression):
501
525
  def __init__(self, value):
502
526
  self.value = value
@@ -519,6 +543,25 @@ class ThisExpression(Expression):
519
543
  return "ThisExpression()"
520
544
 
521
545
 
546
+ class DestructurePattern(Expression):
547
+ """Destructuring pattern for let/const assignments.
548
+
549
+ kind='map': let {a, b} = expr or let {a: x, b: y} = expr
550
+ kind='list': let [x, y, z] = expr
551
+
552
+ bindings: list of (source_key, target_name) tuples
553
+ - map: [("a", "a"), ("b", "renamed")] for {a, b: renamed}
554
+ - list: [(0, "x"), (1, "y")] for [x, y]
555
+ rest: optional name for rest element (..rest)
556
+ """
557
+ def __init__(self, kind, bindings, rest=None):
558
+ self.kind = kind # 'map' or 'list'
559
+ self.bindings = bindings # list of (source, target_name_str)
560
+ self.rest = rest # optional rest variable name
561
+
562
+ def __repr__(self):
563
+ return f"DestructurePattern(kind={self.kind}, bindings={len(self.bindings)}, rest={self.rest})"
564
+
522
565
  class ListLiteral(Expression):
523
566
  def __init__(self, elements):
524
567
  self.elements = elements
@@ -647,6 +690,17 @@ class PropertyAccessExpression(Expression):
647
690
  def __repr__(self):
648
691
  return f"PropertyAccessExpression(object={self.object}, property={self.property}, computed={self.computed})"
649
692
 
693
+
694
+ class SliceExpression(Expression):
695
+ """Slice expression: obj[start:end]"""
696
+ def __init__(self, object, start=None, end=None):
697
+ self.object = object
698
+ self.start = start
699
+ self.end = end
700
+
701
+ def __repr__(self):
702
+ return f"SliceExpression(object={self.object}, start={self.start}, end={self.end})"
703
+
650
704
  class AssignmentExpression(Expression):
651
705
  def __init__(self, name, value):
652
706
  self.name = name
@@ -1163,9 +1217,6 @@ class WatchStatement(Statement):
1163
1217
  def __repr__(self):
1164
1218
  return f"WatchStatement(expr={self.watched_expr}, reaction={self.reaction})"
1165
1219
 
1166
- def __repr__(self):
1167
- return f"WatchStatement(watch={self.watched_expr})"
1168
-
1169
1220
 
1170
1221
  class LogStatement(Statement):
1171
1222
  """Log statement - Redirect output to file
@@ -1574,12 +1625,13 @@ class StateStatement(Statement):
1574
1625
  state owner = TX.caller;
1575
1626
  state locked = false;
1576
1627
  """
1577
- def __init__(self, name, initial_value=None):
1628
+ def __init__(self, name, initial_value=None, modifiers=None):
1578
1629
  self.name = name # Identifier: state variable name
1579
1630
  self.initial_value = initial_value # Optional Expression: initial value
1631
+ self.modifiers = modifiers or [] # List[str]: modifiers (private, public, etc.)
1580
1632
 
1581
1633
  def __repr__(self):
1582
- return f"StateStatement(name={self.name}, initial={self.initial_value})"
1634
+ return f"StateStatement(name={self.name}, initial={self.initial_value}, modifiers={self.modifiers})"
1583
1635
 
1584
1636
 
1585
1637
  class ContractStatement(Statement):
@@ -32,6 +32,16 @@ OR = "||"
32
32
  QUESTION = "?" # Ternary operator: condition ? true_val : false_val
33
33
  NULLISH = "??" # Nullish coalescing: value ?? default
34
34
 
35
+ # Compound Assignment Operators
36
+ PLUS_ASSIGN = "+=" # Addition assignment: x += 5
37
+ MINUS_ASSIGN = "-=" # Subtraction assignment: x -= 5
38
+ STAR_ASSIGN = "*=" # Multiplication assignment: x *= 5
39
+ SLASH_ASSIGN = "/=" # Division assignment: x /= 5
40
+ MOD_ASSIGN = "%=" # Modulo assignment: x %= 5
41
+ POWER = "**" # Exponentiation: x ** 2
42
+ POWER_ASSIGN = "**=" # Exponentiation assignment: x **= 2
43
+ INTERP_STRING = "INTERP_STRING" # String interpolation: "hello ${name}"
44
+
35
45
  # Delimiters
36
46
  COMMA = ","
37
47
  SEMICOLON = ";"
@@ -87,6 +97,7 @@ LAMBDA = "LAMBDA"
87
97
  DEBUG = "DEBUG" # NEW: Debug token
88
98
  TRY = "TRY" # NEW: Try token
89
99
  CATCH = "CATCH" # NEW: Catch token
100
+ FINALLY = "FINALLY" # NEW: Finally token
90
101
  CONTINUE = "CONTINUE" # NEW: Continue on error token
91
102
  BREAK = "BREAK" # NEW: Break loop statement
92
103
  THROW = "THROW" # NEW: Throw error statement
@@ -177,7 +188,6 @@ LIMIT = "LIMIT" # Gas/resource limit: action transfer() limit 100
177
188
  GAS = "GAS" # Gas tracking: gas_used(), gas_remaining()
178
189
  PERSISTENT = "PERSISTENT"
179
190
  STORAGE = "STORAGE"
180
- REQUIRE = "REQUIRE"
181
191
 
182
192
  # PERFORMANCE OPTIMIZATION TOKENS
183
193
  NATIVE = "NATIVE" # Call C/C++ code: native { "func_name", arg1, arg2 }
@@ -190,9 +200,10 @@ SIMD = "SIMD" # Vector operations: simd(operation, vector1, vecto
190
200
  DEFER = "DEFER" # Cleanup code execution: defer cleanup_code;
191
201
  PATTERN = "PATTERN" # Pattern matching: pattern value { case x => ...; }
192
202
  MATCH = "MATCH" # Match expression: match value { Point(x, y) => ... }
203
+ CASE = "CASE" # Case clause in match/pattern
204
+ DEFAULT = "DEFAULT" # Default case in match
193
205
 
194
206
  # ADVANCED FEATURES TOKENS
195
- ENUM = "ENUM" # Type-safe enumerations: enum Color { Red, Green, Blue }
196
207
  STREAM = "STREAM" # Event streaming: stream name as event => handler;
197
208
  WATCH = "WATCH" # Reactive state management: watch variable => reaction;
198
209
  LOG = "LOG" # Output logging: log > filename.txt
@@ -201,9 +212,6 @@ LOG = "LOG" # Output logging: log > filename.txt
201
212
  PUBLIC = "PUBLIC"
202
213
  PRIVATE = "PRIVATE"
203
214
  SEALED = "SEALED"
204
- ASYNC = "ASYNC"
205
- NATIVE = "NATIVE"
206
- INLINE = "INLINE"
207
215
  SECURE = "SECURE"
208
216
  PURE = "PURE"
209
217
  VIEW = "VIEW" # View function (alias for pure, read-only)
@@ -4,6 +4,7 @@ Package Installer - Handles package installation and dependencies
4
4
  import os
5
5
  import json
6
6
  import shutil
7
+ import tarfile
7
8
  from pathlib import Path
8
9
  from typing import Dict, Optional
9
10
 
@@ -11,9 +12,10 @@ from typing import Dict, Optional
11
12
  class PackageInstaller:
12
13
  """Handles package installation"""
13
14
 
14
- def __init__(self, install_dir: Path):
15
+ def __init__(self, install_dir: Path, registry=None):
15
16
  self.install_dir = Path(install_dir)
16
17
  self.install_dir.mkdir(parents=True, exist_ok=True)
18
+ self.registry = registry # PackageRegistry for tarball downloads
17
19
 
18
20
  def install(self, package_info: Dict) -> bool:
19
21
  """Install a package"""
@@ -40,14 +42,12 @@ class PackageInstaller:
40
42
  if pkg_type == "builtin":
41
43
  self._install_builtin(name, version, target_dir)
42
44
  else:
43
- # TODO: Download and extract package
44
45
  self._install_from_source(package_info, target_dir)
45
46
 
46
47
  return True
47
48
 
48
49
  def _install_builtin(self, name: str, version: str, target_dir: Path):
49
50
  """Install a built-in package"""
50
- # Create package.json
51
51
  pkg_json = {
52
52
  "name": name,
53
53
  "version": version,
@@ -58,7 +58,6 @@ class PackageInstaller:
58
58
  with open(target_dir / "zexus.json", "w") as f:
59
59
  json.dump(pkg_json, f, indent=2)
60
60
 
61
- # Create stub main file
62
61
  main_file = target_dir / "index.zx"
63
62
  main_file.write_text(f"""// {name} - Built-in Zexus package
64
63
  // Version: {version}
@@ -72,23 +71,64 @@ export {{
72
71
  """)
73
72
 
74
73
  def _install_from_source(self, package_info: Dict, target_dir: Path):
75
- """Install package from source/tarball"""
76
- # TODO: Implement actual download and extraction
77
-
78
- # For now, create placeholder
74
+ """Install package from a remote tarball or local path.
75
+
76
+ If the package metadata contains a ``tarball`` key pointing to a
77
+ local file, that tarball is extracted directly. Otherwise the
78
+ installer attempts to download the tarball from the registry via
79
+ ``registry.download_tarball(name, version)``.
80
+ """
81
+ name = package_info["name"]
82
+ version = package_info["version"]
83
+
84
+ # 1. Check for a local tarball path in the metadata
85
+ tarball_path = package_info.get("tarball")
86
+ if tarball_path and os.path.isfile(tarball_path):
87
+ self._extract_tarball(Path(tarball_path), target_dir, name)
88
+ return
89
+
90
+ # 2. Try to download from the remote registry
91
+ if self.registry is not None:
92
+ downloaded = self.registry.download_tarball(name, version)
93
+ if downloaded and downloaded.exists():
94
+ print(f"📦 Downloaded {name}@{version}")
95
+ self._extract_tarball(downloaded, target_dir, name)
96
+ return
97
+
98
+ # 3. Fallback — create placeholder with metadata
99
+ print(f"⚠️ Could not download {name}@{version} — creating placeholder")
79
100
  pkg_json = {
80
- "name": package_info["name"],
81
- "version": package_info["version"],
101
+ "name": name,
102
+ "version": version,
82
103
  "description": package_info.get("description", ""),
83
104
  }
84
-
85
105
  with open(target_dir / "zexus.json", "w") as f:
86
106
  json.dump(pkg_json, f, indent=2)
87
-
88
107
  main_file = target_dir / "index.zx"
89
- main_file.write_text(f"""// {package_info['name']}
90
- // Placeholder - package installation from remote sources not yet implemented
91
- """)
108
+ main_file.write_text(f'// {name}@{version} — placeholder (tarball not available)\n')
109
+
110
+ @staticmethod
111
+ def _extract_tarball(tarball_path: Path, target_dir: Path, package_name: str):
112
+ """Extract a ``.tar.gz`` tarball into *target_dir*.
113
+
114
+ Tarballs created by ``PackagePublisher`` contain files under a
115
+ ``<package_name>/`` prefix. We strip that prefix so the files
116
+ land directly in *target_dir*.
117
+ """
118
+ with tarfile.open(tarball_path, "r:gz") as tar:
119
+ # Security: filter out absolute paths and ..'s
120
+ safe_members = []
121
+ prefix = f"{package_name}/"
122
+ for member in tar.getmembers():
123
+ if member.name.startswith("/") or ".." in member.name:
124
+ continue
125
+ # Strip the package-name prefix from the archive path
126
+ if member.name.startswith(prefix):
127
+ member.name = member.name[len(prefix):]
128
+ elif member.name == package_name:
129
+ continue # skip the bare directory entry
130
+ safe_members.append(member)
131
+ tar.extractall(path=target_dir, members=safe_members)
92
132
 
93
133
  def uninstall(self, package: str) -> bool:
94
134
  """Uninstall a package"""
@@ -23,7 +23,7 @@ class PackageManager:
23
23
  self.installer = PackageInstaller(self.zpm_dir)
24
24
  self.publisher = PackagePublisher(self.registry)
25
25
 
26
- def init(self, name: str = None, version: str = "1.7.1") -> Dict:
26
+ def init(self, name: str = None, version: str = "1.7.2") -> Dict:
27
27
  """Initialize a new Zexus project with package.json"""
28
28
  if self.config_file.exists():
29
29
  print(f"⚠️ {self.config_file} already exists")
@@ -1,27 +1,136 @@
1
1
  """
2
2
  Package Registry - Manages package discovery and metadata
3
+
4
+ Supports both a local cache and a remote HTTP registry.
5
+ The registry URL defaults to ``https://registry.zexus.dev`` and can be
6
+ overridden via the ``ZPM_REGISTRY`` environment variable.
7
+
8
+ Auth tokens are read from ``~/.zpm/auth_token`` or the ``ZPM_AUTH_TOKEN``
9
+ environment variable.
10
+
11
+ REST API contract
12
+ -----------------
13
+ The following endpoints are expected on the registry server:
14
+
15
+ GET /packages/<name> → package metadata (latest)
16
+ GET /packages/<name>/<version> → package metadata for version
17
+ GET /packages/<name>/versions → {"versions": ["0.1.0", …]}
18
+ GET /search?q=<query> → {"results": [{…}, …]}
19
+ POST /packages → publish (multipart: meta + tarball)
20
+ GET /packages/<name>/<version>/download → tarball stream
3
21
  """
4
22
  import os
5
23
  import json
24
+ import urllib.request
25
+ import urllib.error
26
+ import urllib.parse
6
27
  from pathlib import Path
7
28
  from typing import Dict, List, Optional
8
29
 
9
30
 
31
+ class RegistryError(Exception):
32
+ """Raised when a registry operation fails."""
33
+
34
+
10
35
  class PackageRegistry:
11
36
  """Package registry for discovering and managing packages"""
12
37
 
13
38
  def __init__(self, registry_url: str = None):
14
- self.registry_url = registry_url or os.environ.get(
15
- "ZPM_REGISTRY",
16
- "https://registry.zexus.dev" # Default registry
17
- )
39
+ self.registry_url = (
40
+ registry_url
41
+ or os.environ.get("ZPM_REGISTRY", "https://registry.zexus.dev")
42
+ ).rstrip("/")
18
43
 
19
44
  # Local cache directory
20
45
  self.cache_dir = Path.home() / ".zpm" / "cache"
21
46
  self.cache_dir.mkdir(parents=True, exist_ok=True)
22
47
 
48
+ # Auth token for publish operations
49
+ self._auth_token: Optional[str] = None
50
+
23
51
  # Built-in packages
24
52
  self.builtin_packages = self._load_builtin_packages()
53
+
54
+ # ------------------------------------------------------------------
55
+ # Auth helpers
56
+ # ------------------------------------------------------------------
57
+
58
+ @property
59
+ def auth_token(self) -> Optional[str]:
60
+ """Lazily resolve the auth token from env or disk."""
61
+ if self._auth_token is not None:
62
+ return self._auth_token
63
+ # 1. Environment variable
64
+ token = os.environ.get("ZPM_AUTH_TOKEN")
65
+ if token:
66
+ self._auth_token = token
67
+ return token
68
+ # 2. Token file
69
+ token_path = Path.home() / ".zpm" / "auth_token"
70
+ if token_path.exists():
71
+ token = token_path.read_text().strip()
72
+ if token:
73
+ self._auth_token = token
74
+ return token
75
+ return None
76
+
77
+ def login(self, token: str) -> None:
78
+ """Store an auth token to ``~/.zpm/auth_token``."""
79
+ self._auth_token = token
80
+ token_path = Path.home() / ".zpm" / "auth_token"
81
+ token_path.parent.mkdir(parents=True, exist_ok=True)
82
+ token_path.write_text(token + "\n")
83
+ # Restrict permissions (owner-only read/write)
84
+ try:
85
+ token_path.chmod(0o600)
86
+ except OSError:
87
+ pass
88
+
89
+ # ------------------------------------------------------------------
90
+ # HTTP helpers
91
+ # ------------------------------------------------------------------
92
+
93
+ def _request(self, path: str, *, method: str = "GET",
94
+ data: bytes = None, headers: dict = None,
95
+ timeout: int = 30) -> Optional[bytes]:
96
+ """Perform an HTTP request against the registry.
97
+
98
+ Returns the response body on success, or ``None`` when the
99
+ resource is not found (HTTP 404). Raises ``RegistryError`` for
100
+ other HTTP errors or connection failures.
101
+ """
102
+ url = f"{self.registry_url}{path}"
103
+ hdrs = {"User-Agent": "zpm/1.0"}
104
+ if self.auth_token:
105
+ hdrs["Authorization"] = f"Bearer {self.auth_token}"
106
+ if headers:
107
+ hdrs.update(headers)
108
+
109
+ req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
110
+ try:
111
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
112
+ return resp.read()
113
+ except urllib.error.HTTPError as exc:
114
+ if exc.code == 404:
115
+ return None
116
+ raise RegistryError(
117
+ f"Registry HTTP {exc.code} for {method} {path}: {exc.reason}"
118
+ ) from exc
119
+ except urllib.error.URLError as exc:
120
+ raise RegistryError(
121
+ f"Cannot reach registry at {self.registry_url}: {exc.reason}"
122
+ ) from exc
123
+
124
+ def _get_json(self, path: str) -> Optional[Dict]:
125
+ """GET ``path`` and parse the JSON body. Returns None on 404."""
126
+ body = self._request(path)
127
+ if body is None:
128
+ return None
129
+ return json.loads(body)
130
+
131
+ # ------------------------------------------------------------------
132
+ # Built-in packages
133
+ # ------------------------------------------------------------------
25
134
 
26
135
  def _load_builtin_packages(self) -> Dict:
27
136
  """Load built-in package definitions"""
@@ -55,56 +164,176 @@ class PackageRegistry:
55
164
  "files": []
56
165
  }
57
166
  }
167
+
168
+ # ------------------------------------------------------------------
169
+ # Public API
170
+ # ------------------------------------------------------------------
58
171
 
59
172
  def get_package(self, name: str, version: str = "latest") -> Optional[Dict]:
60
- """Get package metadata from registry"""
61
- # Check built-in packages first
173
+ """Get package metadata from registry.
174
+
175
+ Resolution order:
176
+ 1. Built-in packages
177
+ 2. Local cache (``~/.zpm/cache/<name>-<version>.json``)
178
+ 3. Remote registry (``GET /packages/<name>[/<version>]``)
179
+ """
180
+ # 1. Check built-in packages
62
181
  if name in self.builtin_packages:
63
182
  return self.builtin_packages[name]
64
183
 
65
- # Check cache
184
+ # 2. Check local cache
66
185
  cache_file = self.cache_dir / f"{name}-{version}.json"
67
186
  if cache_file.exists():
68
187
  with open(cache_file) as f:
69
188
  return json.load(f)
70
189
 
71
- # TODO: Fetch from remote registry
72
- # For now, return None if not found
190
+ # 3. Fetch from remote registry
191
+ try:
192
+ path = f"/packages/{urllib.parse.quote(name)}"
193
+ if version and version != "latest":
194
+ path += f"/{urllib.parse.quote(version)}"
195
+ meta = self._get_json(path)
196
+ if meta:
197
+ # Cache the result locally
198
+ self._cache_metadata(name, meta.get("version", version), meta)
199
+ return meta
200
+ except RegistryError as e:
201
+ # Log but don't crash — fall through to None
202
+ print(f"⚠️ Registry lookup failed for {name}: {e}")
203
+
73
204
  return None
74
205
 
75
206
  def search(self, query: str) -> List[Dict]:
76
- """Search for packages"""
207
+ """Search for packages.
208
+
209
+ Searches built-in packages locally *and* queries the remote
210
+ registry via ``GET /search?q=<query>``.
211
+ """
77
212
  results = []
78
213
 
79
214
  # Search built-in packages
215
+ q = query.lower()
80
216
  for name, pkg in self.builtin_packages.items():
81
- if query.lower() in name.lower() or query.lower() in pkg.get("description", "").lower():
217
+ if q in name.lower() or q in pkg.get("description", "").lower():
82
218
  results.append(pkg)
83
219
 
84
- # TODO: Search remote registry
220
+ # Search remote registry
221
+ try:
222
+ encoded = urllib.parse.urlencode({"q": query})
223
+ data = self._get_json(f"/search?{encoded}")
224
+ if data and isinstance(data.get("results"), list):
225
+ for pkg in data["results"]:
226
+ # Deduplicate against builtins
227
+ if pkg.get("name") not in self.builtin_packages:
228
+ results.append(pkg)
229
+ except RegistryError as e:
230
+ print(f"⚠️ Remote search failed: {e}")
85
231
 
86
232
  return results
87
233
 
88
234
  def publish_package(self, package_data: Dict, files: List[str]) -> bool:
89
- """Publish a package to registry"""
90
- # For now, just cache locally
91
- name = package_data["name"]
92
- version = package_data["version"]
93
-
94
- cache_file = self.cache_dir / f"{name}-{version}.json"
95
- with open(cache_file, "w") as f:
96
- json.dump(package_data, f, indent=2)
97
-
98
- print(f"✅ Package cached locally at {cache_file}")
99
- print(f"⚠️ Remote registry publication not yet implemented")
100
-
101
- # TODO: Upload to remote registry
102
- return True
235
+ """Publish a package to the registry.
236
+
237
+ Sends a multipart POST to ``/packages`` with the package metadata
238
+ (JSON) and the tarball (binary). Requires an auth token.
239
+ """
240
+ name = package_data.get("name", "")
241
+ version = package_data.get("version", "")
242
+
243
+ # Always cache locally first
244
+ self._cache_metadata(name, version, package_data)
245
+
246
+ # Attempt remote publish
247
+ if not self.auth_token:
248
+ print(f"⚠️ No auth token — package cached locally only.")
249
+ print(f" Run 'zpm login <token>' to enable remote publishing.")
250
+ return True
251
+
252
+ tarball_path = package_data.get("tarball")
253
+ try:
254
+ import mimetypes
255
+ boundary = "----ZPMPublishBoundary"
256
+ body_parts = []
257
+
258
+ # Part 1: metadata JSON
259
+ body_parts.append(f"--{boundary}".encode())
260
+ body_parts.append(b'Content-Disposition: form-data; name="metadata"')
261
+ body_parts.append(b"Content-Type: application/json")
262
+ body_parts.append(b"")
263
+ body_parts.append(json.dumps(package_data).encode())
264
+
265
+ # Part 2: tarball file (if present)
266
+ if tarball_path and os.path.isfile(tarball_path):
267
+ body_parts.append(f"--{boundary}".encode())
268
+ body_parts.append(
269
+ f'Content-Disposition: form-data; name="tarball"; '
270
+ f'filename="{os.path.basename(tarball_path)}"'.encode()
271
+ )
272
+ body_parts.append(b"Content-Type: application/gzip")
273
+ body_parts.append(b"")
274
+ with open(tarball_path, "rb") as f:
275
+ body_parts.append(f.read())
276
+
277
+ body_parts.append(f"--{boundary}--".encode())
278
+ body = b"\r\n".join(body_parts)
279
+
280
+ self._request(
281
+ "/packages",
282
+ method="POST",
283
+ data=body,
284
+ headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
285
+ )
286
+ print(f"✅ Published {name}@{version} to {self.registry_url}")
287
+ return True
288
+ except RegistryError as e:
289
+ print(f"❌ Remote publish failed: {e}")
290
+ print(f" Package was cached locally at ~/.zpm/cache/")
291
+ return False
103
292
 
104
293
  def get_versions(self, package: str) -> List[str]:
105
- """Get all available versions of a package"""
294
+ """Get all available versions of a package."""
106
295
  if package in self.builtin_packages:
107
296
  return [self.builtin_packages[package]["version"]]
108
297
 
109
- # TODO: Query remote registry
298
+ # Check remote registry
299
+ try:
300
+ encoded = urllib.parse.quote(package)
301
+ data = self._get_json(f"/packages/{encoded}/versions")
302
+ if data and isinstance(data.get("versions"), list):
303
+ return data["versions"]
304
+ except RegistryError as e:
305
+ print(f"⚠️ Version lookup failed for {package}: {e}")
306
+
110
307
  return []
308
+
309
+ def download_tarball(self, name: str, version: str) -> Optional[Path]:
310
+ """Download a package tarball from the registry.
311
+
312
+ Returns the local path to the downloaded ``.tar.gz`` file,
313
+ or ``None`` on failure.
314
+ """
315
+ tarball_cache = self.cache_dir / f"{name}-{version}.tar.gz"
316
+ if tarball_cache.exists():
317
+ return tarball_cache
318
+
319
+ try:
320
+ encoded_name = urllib.parse.quote(name)
321
+ encoded_ver = urllib.parse.quote(version)
322
+ body = self._request(f"/packages/{encoded_name}/{encoded_ver}/download")
323
+ if body is None:
324
+ return None
325
+ tarball_cache.write_bytes(body)
326
+ return tarball_cache
327
+ except RegistryError as e:
328
+ print(f"⚠️ Tarball download failed for {name}@{version}: {e}")
329
+ return None
330
+
331
+ # ------------------------------------------------------------------
332
+ # Internal helpers
333
+ # ------------------------------------------------------------------
334
+
335
+ def _cache_metadata(self, name: str, version: str, meta: Dict) -> None:
336
+ """Write package metadata to the local cache."""
337
+ cache_file = self.cache_dir / f"{name}-{version}.json"
338
+ with open(cache_file, "w") as f:
339
+ json.dump(meta, f, indent=2)