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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
package/src/zexus/zexus_ast.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
package/src/zexus/zexus_token.py
CHANGED
|
@@ -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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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":
|
|
81
|
-
"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
|
|
90
|
-
|
|
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.
|
|
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 =
|
|
15
|
-
|
|
16
|
-
"https://registry.zexus.dev"
|
|
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
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
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
|
|
217
|
+
if q in name.lower() or q in pkg.get("description", "").lower():
|
|
82
218
|
results.append(pkg)
|
|
83
219
|
|
|
84
|
-
#
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
#
|
|
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)
|