jvcli 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jvcli/__init__.py +8 -0
- jvcli/api.py +318 -0
- jvcli/auth.py +45 -0
- jvcli/cli.py +36 -0
- jvcli/client/assets/index-BtFItD2q.js +156 -0
- jvcli/client/assets/index-CIEsu-TC.css +1 -0
- jvcli/client/index.html +15 -0
- jvcli/client/jac_logo.png +0 -0
- jvcli/client/tauri.svg +6 -0
- jvcli/client/vite.svg +1 -0
- jvcli/commands/__init__.py +1 -0
- jvcli/commands/auth.py +46 -0
- jvcli/commands/create.py +452 -0
- jvcli/commands/download.py +111 -0
- jvcli/commands/info.py +91 -0
- jvcli/commands/publish.py +207 -0
- jvcli/commands/studio.py +96 -0
- jvcli/commands/update.py +67 -0
- jvcli/templates/2.0.0/action_info.yaml +14 -0
- jvcli/templates/2.0.0/agent_descriptor.yaml +30 -0
- jvcli/templates/2.0.0/agent_info.yaml +10 -0
- jvcli/templates/2.0.0/agent_knowledge.yaml +14 -0
- jvcli/templates/2.0.0/agent_memory.yaml +1 -0
- jvcli/templates/CHANGELOG.md +2 -0
- jvcli/templates/README.md +4 -0
- jvcli/utils.py +209 -0
- jvcli-2.0.0.dist-info/LICENSE +201 -0
- jvcli-2.0.0.dist-info/METADATA +24 -0
- jvcli-2.0.0.dist-info/RECORD +32 -0
- jvcli-2.0.0.dist-info/WHEEL +5 -0
- jvcli-2.0.0.dist-info/entry_points.txt +2 -0
- jvcli-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
"""Publish command group for the Jivas Package Repository CLI."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import tempfile
|
5
|
+
|
6
|
+
import click
|
7
|
+
from pyaml import yaml
|
8
|
+
|
9
|
+
from jvcli.api import RegistryAPI
|
10
|
+
from jvcli.auth import load_token
|
11
|
+
from jvcli.utils import (
|
12
|
+
compress_package_to_tgz,
|
13
|
+
validate_dependencies,
|
14
|
+
validate_package_name,
|
15
|
+
validate_yaml_format,
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
# Main `publish` group command
|
20
|
+
@click.group()
|
21
|
+
def publish() -> None:
|
22
|
+
"""
|
23
|
+
Publish resources to the Jivas environment.
|
24
|
+
Available subcommands: action, agent.
|
25
|
+
"""
|
26
|
+
pass # pragma: no cover
|
27
|
+
|
28
|
+
|
29
|
+
@publish.command(name="action")
|
30
|
+
@click.option(
|
31
|
+
"--path",
|
32
|
+
required=True,
|
33
|
+
help="Path to the directory containing the action to publish.",
|
34
|
+
)
|
35
|
+
@click.option(
|
36
|
+
"--visibility",
|
37
|
+
type=click.Choice(["public", "private"], case_sensitive=False),
|
38
|
+
default="public",
|
39
|
+
show_default=True,
|
40
|
+
help="Visibility of the published action (public or private).",
|
41
|
+
)
|
42
|
+
@click.option(
|
43
|
+
"--package-only",
|
44
|
+
is_flag=True,
|
45
|
+
default=False,
|
46
|
+
show_default=True,
|
47
|
+
help="Only generate the package without publishing.",
|
48
|
+
)
|
49
|
+
@click.option(
|
50
|
+
"--output",
|
51
|
+
"-o",
|
52
|
+
default=None,
|
53
|
+
required=False,
|
54
|
+
help="Output path for generated package.",
|
55
|
+
)
|
56
|
+
@click.option(
|
57
|
+
"--namespace",
|
58
|
+
required=False, # Initially not required
|
59
|
+
help="Namespace of the package (required when --path is a tarball).",
|
60
|
+
)
|
61
|
+
def publish_action(
|
62
|
+
path: str, visibility: str, package_only: bool, output: str, namespace: str
|
63
|
+
) -> None:
|
64
|
+
"""Publish an action to the Jivas environment."""
|
65
|
+
|
66
|
+
if path.endswith(".tar.gz") and not namespace:
|
67
|
+
raise ValueError("--namespace is required when --path is a tarball (.tar.gz).")
|
68
|
+
|
69
|
+
_publish_common(path, visibility, package_only, output, "action", namespace)
|
70
|
+
|
71
|
+
|
72
|
+
@publish.command(name="agent")
|
73
|
+
@click.option(
|
74
|
+
"--path",
|
75
|
+
required=True,
|
76
|
+
help="Path to the directory containing the agent to publish.",
|
77
|
+
)
|
78
|
+
@click.option(
|
79
|
+
"--visibility",
|
80
|
+
type=click.Choice(["public", "private"], case_sensitive=False),
|
81
|
+
default="public",
|
82
|
+
show_default=True,
|
83
|
+
help="Visibility of the published agent (public or private).",
|
84
|
+
)
|
85
|
+
@click.option(
|
86
|
+
"--package-only",
|
87
|
+
is_flag=True,
|
88
|
+
default=False,
|
89
|
+
show_default=True,
|
90
|
+
help="Only generate the package without publishing.",
|
91
|
+
)
|
92
|
+
@click.option(
|
93
|
+
"--output",
|
94
|
+
"-o",
|
95
|
+
required=False,
|
96
|
+
help="Output path for generated package.",
|
97
|
+
)
|
98
|
+
@click.option(
|
99
|
+
"--namespace",
|
100
|
+
required=False, # Initially not required
|
101
|
+
help="Namespace of the package (required when --path is a tarball).",
|
102
|
+
)
|
103
|
+
def publish_agent(
|
104
|
+
path: str, visibility: str, package_only: bool, output: str, namespace: str
|
105
|
+
) -> None:
|
106
|
+
"""Publish an agent to the Jivas environment."""
|
107
|
+
|
108
|
+
if path.endswith(".tar.gz") and not namespace:
|
109
|
+
raise ValueError("--namespace is required when --path is a tarball (.tar.gz).")
|
110
|
+
|
111
|
+
_publish_common(path, visibility, package_only, output, "agent", namespace)
|
112
|
+
|
113
|
+
|
114
|
+
def _publish_common(
|
115
|
+
path: str,
|
116
|
+
visibility: str,
|
117
|
+
package_only: bool,
|
118
|
+
output: str,
|
119
|
+
publish_type: str,
|
120
|
+
namespace: str,
|
121
|
+
) -> None:
|
122
|
+
|
123
|
+
token = load_token().get("token")
|
124
|
+
if not token and not package_only:
|
125
|
+
click.secho("You need to login first.", fg="red")
|
126
|
+
return
|
127
|
+
|
128
|
+
# Check if path is directory
|
129
|
+
if os.path.isdir(path):
|
130
|
+
|
131
|
+
info_path = os.path.join(path, "info.yaml")
|
132
|
+
if not os.path.exists(info_path):
|
133
|
+
click.secho(
|
134
|
+
f"Error: 'info.yaml' not found in the directory '{path}'.", fg="red"
|
135
|
+
)
|
136
|
+
return
|
137
|
+
|
138
|
+
click.secho(f"Preparing {publish_type} from directory: {path}", fg="yellow")
|
139
|
+
|
140
|
+
with open(info_path, "r") as info_file:
|
141
|
+
info_data = yaml.safe_load(info_file)
|
142
|
+
|
143
|
+
if validate_yaml_format(info_data, type=publish_type):
|
144
|
+
click.secho("info.yaml validated successfully.", fg="yellow")
|
145
|
+
else:
|
146
|
+
click.secho(f"Error validating 'info.yaml' for {publish_type}.", fg="red")
|
147
|
+
return
|
148
|
+
|
149
|
+
try:
|
150
|
+
package_name = info_data["package"].get("name")
|
151
|
+
validate_package_name(package_name)
|
152
|
+
click.secho(
|
153
|
+
f"Package name '{package_name}' validated successfully.", fg="yellow"
|
154
|
+
)
|
155
|
+
except ValueError as e:
|
156
|
+
click.secho(f"Error validating package name: {e}", fg="red")
|
157
|
+
return
|
158
|
+
|
159
|
+
try:
|
160
|
+
validate_dependencies(info_data["package"].get("dependencies", {}))
|
161
|
+
click.secho("Dependencies validated successfully.", fg="yellow")
|
162
|
+
except ValueError as e:
|
163
|
+
click.secho(f"Error validating dependencies: {e}", fg="red")
|
164
|
+
return
|
165
|
+
|
166
|
+
namespace, name = package_name.split("/", 1)
|
167
|
+
|
168
|
+
if package_only and not output:
|
169
|
+
output = "."
|
170
|
+
|
171
|
+
tgz_file_path = _prepare_package(namespace, name, path, publish_type, output)
|
172
|
+
click.secho(f"Compressed {publish_type} to: {tgz_file_path}", fg="yellow")
|
173
|
+
|
174
|
+
# check if path is already a tgz file
|
175
|
+
elif path.endswith(".tar.gz"):
|
176
|
+
click.secho(f"Preparing {publish_type} from tgz file: {path}", fg="yellow")
|
177
|
+
tgz_file_path = path
|
178
|
+
|
179
|
+
else:
|
180
|
+
click.secho(
|
181
|
+
f"Unable to publish {publish_type} from the path: {path}, unsupported file format"
|
182
|
+
)
|
183
|
+
return
|
184
|
+
|
185
|
+
if not package_only:
|
186
|
+
click.secho(
|
187
|
+
f"Publishing {publish_type} with visibility: {visibility}", fg="blue"
|
188
|
+
)
|
189
|
+
response = RegistryAPI.publish_action(
|
190
|
+
tgz_file_path, visibility, str(token), namespace or ""
|
191
|
+
)
|
192
|
+
if response:
|
193
|
+
click.secho(
|
194
|
+
f"{publish_type.capitalize()} published successfully!", fg="green"
|
195
|
+
)
|
196
|
+
|
197
|
+
|
198
|
+
def _prepare_package(
|
199
|
+
namespace: str, name: str, path: str, publish_type: str, output: str
|
200
|
+
) -> str:
|
201
|
+
"""Prepare the package for publishing."""
|
202
|
+
tgz_filename = os.path.join(
|
203
|
+
output if output else tempfile.gettempdir(), f"{namespace}_{name}.tar.gz"
|
204
|
+
)
|
205
|
+
tgz_file_path = compress_package_to_tgz(path, tgz_filename)
|
206
|
+
click.secho(f"Compressed {publish_type} to: {tgz_file_path}", fg="yellow")
|
207
|
+
return tgz_file_path
|
jvcli/commands/studio.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Studio command group for deploying and interfacing with the Jivas Studio."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import click
|
7
|
+
import jaclang # noqa: F401
|
8
|
+
from bson import ObjectId
|
9
|
+
from jac_cloud.core.architype import NodeAnchor
|
10
|
+
from uvicorn import run
|
11
|
+
|
12
|
+
|
13
|
+
@click.group()
|
14
|
+
def studio() -> None:
|
15
|
+
"""Group for managing Jivas Studio resources."""
|
16
|
+
pass # pragma: no cover
|
17
|
+
|
18
|
+
|
19
|
+
def get_graph(root: str) -> dict:
|
20
|
+
"""Fetches a graph structure from the database."""
|
21
|
+
nodes = []
|
22
|
+
edges = []
|
23
|
+
|
24
|
+
edge_collection = NodeAnchor.Collection.get_collection("edge")
|
25
|
+
node_collection = NodeAnchor.Collection.get_collection("node")
|
26
|
+
node_docs = node_collection.find({"root": ObjectId(root)})
|
27
|
+
edge_docs = edge_collection.find({"root": ObjectId(root)})
|
28
|
+
|
29
|
+
for node in node_docs:
|
30
|
+
nodes.append(
|
31
|
+
{
|
32
|
+
"id": node["_id"],
|
33
|
+
"data": node["architype"],
|
34
|
+
"name": node["name"],
|
35
|
+
}
|
36
|
+
)
|
37
|
+
for edge in edge_docs:
|
38
|
+
edges.append(
|
39
|
+
{
|
40
|
+
"id": edge["_id"],
|
41
|
+
"name": edge["name"],
|
42
|
+
"source": edge["source"],
|
43
|
+
"target": edge["target"],
|
44
|
+
"data": edge["architype"],
|
45
|
+
}
|
46
|
+
)
|
47
|
+
|
48
|
+
return {
|
49
|
+
"nodes": json.loads(json.dumps(nodes, default=str)),
|
50
|
+
"edges": json.loads(json.dumps(edges, default=str)),
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
def get_users() -> list:
|
55
|
+
"""Fetches users from the database."""
|
56
|
+
users = []
|
57
|
+
user_collection = NodeAnchor.Collection.get_collection("user")
|
58
|
+
user_docs = user_collection.find()
|
59
|
+
|
60
|
+
for user in user_docs:
|
61
|
+
users.append(
|
62
|
+
{
|
63
|
+
"id": user["_id"],
|
64
|
+
"root_id": user["root_id"],
|
65
|
+
"email": user["email"],
|
66
|
+
}
|
67
|
+
)
|
68
|
+
|
69
|
+
return json.loads(json.dumps(users, default=str))
|
70
|
+
|
71
|
+
|
72
|
+
@studio.command()
|
73
|
+
@click.option("--port", default=8989, help="Port for the studio to launch on.")
|
74
|
+
def launch(port: int) -> None:
|
75
|
+
"""Launch the Jivas Studio on the specified port."""
|
76
|
+
click.echo(f"Launching Jivas Studio on port {port}...")
|
77
|
+
from fastapi import FastAPI
|
78
|
+
from fastapi.middleware.cors import CORSMiddleware
|
79
|
+
from fastapi.staticfiles import StaticFiles
|
80
|
+
|
81
|
+
app = FastAPI()
|
82
|
+
app.add_middleware(
|
83
|
+
CORSMiddleware,
|
84
|
+
allow_origins=["*"],
|
85
|
+
allow_credentials=True,
|
86
|
+
allow_methods=["*"],
|
87
|
+
allow_headers=["*"],
|
88
|
+
)
|
89
|
+
|
90
|
+
app.add_api_route("/graph", endpoint=get_graph, methods=["GET"])
|
91
|
+
app.add_api_route("/users", endpoint=get_users, methods=["GET"])
|
92
|
+
|
93
|
+
client_dir = Path(__file__).resolve().parent.parent.joinpath("client")
|
94
|
+
app.mount("/", StaticFiles(directory=client_dir, html=True), name="studio")
|
95
|
+
|
96
|
+
run(app, host="0.0.0.0", port=port)
|
jvcli/commands/update.py
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Update command group operations for Jivas Package Repository CLI tool."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from jvcli.api import RegistryAPI
|
6
|
+
from jvcli.auth import load_token
|
7
|
+
|
8
|
+
|
9
|
+
@click.group()
|
10
|
+
def update() -> None:
|
11
|
+
"""Group for updating resources like namespaces."""
|
12
|
+
pass # pragma: no cover
|
13
|
+
|
14
|
+
|
15
|
+
@update.command(name="namespace")
|
16
|
+
@click.argument("namespace", required=True)
|
17
|
+
@click.option(
|
18
|
+
"--invite",
|
19
|
+
type=str,
|
20
|
+
help="Invite a user to the namespace by their email.",
|
21
|
+
metavar="EMAIL",
|
22
|
+
)
|
23
|
+
@click.option(
|
24
|
+
"--transfer",
|
25
|
+
type=str,
|
26
|
+
help="Transfer ownership of the namespace to a specified user by their email.",
|
27
|
+
metavar="EMAIL",
|
28
|
+
)
|
29
|
+
@click.pass_context
|
30
|
+
def namespace(ctx: click.Context, namespace: str, invite: str, transfer: str) -> None:
|
31
|
+
"""
|
32
|
+
Update operations for a specified namespace.
|
33
|
+
Use one of the available options: --invite, --transfer.
|
34
|
+
"""
|
35
|
+
|
36
|
+
token = load_token().get("token")
|
37
|
+
if not token:
|
38
|
+
click.secho("You need to login first.", fg="red")
|
39
|
+
ctx.exit(1)
|
40
|
+
|
41
|
+
# Validate mutually exclusive options
|
42
|
+
if invite and transfer:
|
43
|
+
click.secho(
|
44
|
+
"You can only use one of --invite or --transfer at a time.", fg="red"
|
45
|
+
)
|
46
|
+
ctx.exit(1)
|
47
|
+
|
48
|
+
# Handle different operations
|
49
|
+
if invite:
|
50
|
+
click.secho(f"Inviting '{invite}' to namespace '{namespace}'...", fg="yellow")
|
51
|
+
# Logic to invite a user to the namespace
|
52
|
+
RegistryAPI.invite_user_to_namespace(
|
53
|
+
namespace_name=namespace, user_email=invite, token=str(token)
|
54
|
+
)
|
55
|
+
elif transfer:
|
56
|
+
click.secho(
|
57
|
+
f"Transferring ownership of namespace '{namespace}' to '{transfer}'...",
|
58
|
+
fg="yellow",
|
59
|
+
)
|
60
|
+
# Logic to transfer ownership of the namespace
|
61
|
+
RegistryAPI.transfer_namespace_ownership(
|
62
|
+
namespace_name=namespace, new_owner_email=transfer, token=str(token)
|
63
|
+
)
|
64
|
+
|
65
|
+
click.secho(
|
66
|
+
f"Operation on namespace '{namespace}' completed successfully.", fg="green"
|
67
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
package:
|
2
|
+
name: "{{name}}"
|
3
|
+
author: "{{author}}"
|
4
|
+
architype: "{{architype}}"
|
5
|
+
version: "{{version}}"
|
6
|
+
meta:
|
7
|
+
title: "{{title}}"
|
8
|
+
description: "{{description}}"
|
9
|
+
group: "{{group}}"
|
10
|
+
type: "{{type}}"
|
11
|
+
config:
|
12
|
+
singleton: "{{singleton}}"
|
13
|
+
dependencies:
|
14
|
+
jivas: "~{{jivas_version}}"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
published: true
|
2
|
+
name: "{{name}}"
|
3
|
+
description: |-
|
4
|
+
{{description}}
|
5
|
+
actions:
|
6
|
+
- action: jivas/intro_interact_action
|
7
|
+
context:
|
8
|
+
version: "0.0.1"
|
9
|
+
enabled: true
|
10
|
+
prompt: "In a natural and brief manner:\na. Introduce yourself by name and explain your role."
|
11
|
+
- action: jivas/persona_interact_action
|
12
|
+
context:
|
13
|
+
version: "0.0.1"
|
14
|
+
enabled: true
|
15
|
+
anchors: []
|
16
|
+
timezone: "UTC"
|
17
|
+
history: true
|
18
|
+
user_model: true
|
19
|
+
history_size: 2
|
20
|
+
max_statement_length: 800
|
21
|
+
model_name: "gpt-4o"
|
22
|
+
- action: jivas/agent_utils_action
|
23
|
+
context:
|
24
|
+
version: "0.0.1"
|
25
|
+
enabled: true
|
26
|
+
- action: jivas/langchain_model_action
|
27
|
+
context:
|
28
|
+
version: "0.0.1"
|
29
|
+
enabled: true
|
30
|
+
api_key: ""
|
@@ -0,0 +1,14 @@
|
|
1
|
+
- metadata:
|
2
|
+
knode_index: Example, Knode, Index
|
3
|
+
anchors:
|
4
|
+
- What does this do?
|
5
|
+
- What is JIVAS?
|
6
|
+
text: |2-
|
7
|
+
|
8
|
+
# Example
|
9
|
+
|
10
|
+
## Example Questions:
|
11
|
+
- What is JIVAS?
|
12
|
+
|
13
|
+
## Answer:
|
14
|
+
JIVAS is a framework for building agents that can interact with the world.
|
@@ -0,0 +1 @@
|
|
1
|
+
- memory:
|
jvcli/utils.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
"""Utility functions for the Jivas Package Repository CLI tool."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import tarfile
|
6
|
+
|
7
|
+
import click
|
8
|
+
import yaml
|
9
|
+
from packaging.specifiers import SpecifierSet
|
10
|
+
from packaging.version import parse as parse_version
|
11
|
+
|
12
|
+
from jvcli import __supported__jivas__versions__
|
13
|
+
from jvcli.api import RegistryAPI
|
14
|
+
from jvcli.auth import load_token
|
15
|
+
|
16
|
+
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
17
|
+
|
18
|
+
|
19
|
+
def validate_snake_case(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
20
|
+
"""Validate that the input is in snake_case."""
|
21
|
+
if not re.match(r"^[a-z0-9_]+$", value):
|
22
|
+
raise click.BadParameter(
|
23
|
+
"must be snake_case (lowercase letters, numbers, and underscores only)."
|
24
|
+
)
|
25
|
+
return value
|
26
|
+
|
27
|
+
|
28
|
+
def validate_name(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
29
|
+
"""Validate that the input only contains lowercase letters and numbers. Used for validating names."""
|
30
|
+
if not re.match(r"^[a-z0-9]+$", value):
|
31
|
+
raise click.BadParameter("must be lowercase letters and numbers only.")
|
32
|
+
return value
|
33
|
+
|
34
|
+
|
35
|
+
def validate_yaml_format(info_data: dict, type: str, version: str = "latest") -> bool:
|
36
|
+
"""Validate if the info.yaml data matches the corresponding version template."""
|
37
|
+
if version == "latest":
|
38
|
+
version = max(__supported__jivas__versions__)
|
39
|
+
|
40
|
+
if type == "action" or type.endswith("action"):
|
41
|
+
template_path = os.path.join(TEMPLATES_DIR, version, "action_info.yaml")
|
42
|
+
|
43
|
+
if type == "daf" or type == "agent":
|
44
|
+
template_path = os.path.join(TEMPLATES_DIR, version, "agent_info.yaml")
|
45
|
+
|
46
|
+
if not os.path.exists(template_path):
|
47
|
+
click.secho(f"Template for version {version} not found.", fg="red")
|
48
|
+
return False
|
49
|
+
|
50
|
+
# Load template
|
51
|
+
with open(template_path, "r") as template_file:
|
52
|
+
# Fill placeholders to avoid YAML error
|
53
|
+
template_content = template_file.read().format(
|
54
|
+
dict.fromkeys(info_data.keys(), "")
|
55
|
+
)
|
56
|
+
template_data = yaml.safe_load(template_content)
|
57
|
+
|
58
|
+
# Compare keys
|
59
|
+
if set(info_data.keys()) != set(template_data.keys()):
|
60
|
+
missing_keys = set(template_data.keys()) - set(info_data.keys())
|
61
|
+
extra_keys = set(info_data.keys()) - set(template_data.keys())
|
62
|
+
|
63
|
+
if extra_keys:
|
64
|
+
click.secho(
|
65
|
+
f"Warning: Extra keys: {extra_keys} found in info.yaml, the jivas package repository may ignore them.",
|
66
|
+
fg="yellow",
|
67
|
+
)
|
68
|
+
|
69
|
+
if missing_keys:
|
70
|
+
click.secho(
|
71
|
+
f"info.yaml validation failed. Missing keys: {missing_keys}",
|
72
|
+
fg="red",
|
73
|
+
)
|
74
|
+
return False
|
75
|
+
return True
|
76
|
+
|
77
|
+
|
78
|
+
def validate_package_name(name: str) -> None:
|
79
|
+
"""Ensure the package name includes a namespace and matches user access."""
|
80
|
+
if "/" not in name:
|
81
|
+
raise ValueError(
|
82
|
+
f"Package name '{name}' must include a namespace (e.g., 'namespace/action_name')."
|
83
|
+
)
|
84
|
+
|
85
|
+
namespace, _ = name.split("/", 1)
|
86
|
+
namespaces = load_token().get("namespaces", {}).get("groups", [])
|
87
|
+
if namespace not in namespaces:
|
88
|
+
raise ValueError(
|
89
|
+
f"Namespace '{namespace}' is not accessible to the current user."
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
def is_version_compatible(version: str, specifiers: str) -> bool:
|
94
|
+
"""
|
95
|
+
Compares the provided version to a given set of specifications/modifiers or exact version match.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
- version (str): The version to be compared. E.g., "2.1.0".
|
99
|
+
- specifiers (str): The version specifier set or exact version. E.g., "2.1.0" or ">=0.2,<0.3" or "0.0.1" or "^2.0.0"
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
- bool: True if the version satisfies the specifier set or exact match, False otherwise.
|
103
|
+
"""
|
104
|
+
try:
|
105
|
+
# Parse the version to check
|
106
|
+
version = parse_version(version)
|
107
|
+
|
108
|
+
# Check if specifiers is a simple exact version match
|
109
|
+
try:
|
110
|
+
exact_version = parse_version(specifiers)
|
111
|
+
return version == exact_version
|
112
|
+
except Exception:
|
113
|
+
# If parsing fails, treat it as a specifier set
|
114
|
+
pass
|
115
|
+
|
116
|
+
# Handle "~" shorthand by translating it to a compatible range
|
117
|
+
if specifiers.startswith("~"):
|
118
|
+
base_version = specifiers[1:]
|
119
|
+
parsed_base = parse_version(base_version)
|
120
|
+
major = parsed_base.major
|
121
|
+
minor = parsed_base.minor
|
122
|
+
# Assuming the next release constraint is on minor bump
|
123
|
+
upper_bound = f"<{major}.{minor + 1}"
|
124
|
+
specifiers = f">={base_version},{upper_bound}"
|
125
|
+
|
126
|
+
# Handle "^" shorthand to translate to a compatible range
|
127
|
+
if specifiers.startswith("^"):
|
128
|
+
base_version = specifiers[1:]
|
129
|
+
parsed_base = parse_version(base_version)
|
130
|
+
major = parsed_base.major
|
131
|
+
minor = parsed_base.minor
|
132
|
+
patch = parsed_base.micro
|
133
|
+
if major > 0:
|
134
|
+
upper_bound = f"<{major + 1}.0.0"
|
135
|
+
elif minor > 0:
|
136
|
+
upper_bound = f"<0.{minor + 1}.0"
|
137
|
+
else:
|
138
|
+
upper_bound = f"<0.0.{patch + 1}"
|
139
|
+
specifiers = f">={base_version},{upper_bound}"
|
140
|
+
|
141
|
+
# Create a SpecifierSet with the given specifiers
|
142
|
+
spec_set = SpecifierSet(specifiers)
|
143
|
+
|
144
|
+
# Check if the version matches the specifier set
|
145
|
+
return version in spec_set
|
146
|
+
|
147
|
+
except Exception as e:
|
148
|
+
# Handle exceptions if the inputs are malformed or invalid
|
149
|
+
click.secho(f"Error comparing versions: {e}", fg="red")
|
150
|
+
return False
|
151
|
+
|
152
|
+
|
153
|
+
def validate_dependencies(dependencies: dict) -> None:
|
154
|
+
"""Ensure all dependencies exist in the registry."""
|
155
|
+
missing_dependencies = []
|
156
|
+
for dep, specifier in dependencies.items():
|
157
|
+
if dep == "jivas":
|
158
|
+
# Check if the version is in list of supported versions
|
159
|
+
def supported(spec: str) -> bool:
|
160
|
+
return any(
|
161
|
+
is_version_compatible(version, spec)
|
162
|
+
for version in __supported__jivas__versions__
|
163
|
+
)
|
164
|
+
|
165
|
+
if not supported(specifier):
|
166
|
+
missing_dependencies.append(f"{dep} {specifier}")
|
167
|
+
elif dep == "actions":
|
168
|
+
# Check if action exists in the registry
|
169
|
+
for name, spec in specifier.items():
|
170
|
+
package = RegistryAPI.download_package(
|
171
|
+
name=name, version=spec, suppress_error=True
|
172
|
+
)
|
173
|
+
|
174
|
+
if not package:
|
175
|
+
missing_dependencies.append(f"{dep} {specifier}")
|
176
|
+
elif dep == "pip":
|
177
|
+
# TODO: Add support for pip dependencies
|
178
|
+
continue
|
179
|
+
else:
|
180
|
+
raise ValueError(f"Unknown dependency type: {dep}")
|
181
|
+
|
182
|
+
if missing_dependencies:
|
183
|
+
raise ValueError(f"Dependencies not found in registry: {missing_dependencies}")
|
184
|
+
|
185
|
+
|
186
|
+
def compress_package_to_tgz(source_path: str, output_filename: str) -> str:
|
187
|
+
"""
|
188
|
+
Compress the action folder into a .tgz file with the required structure,
|
189
|
+
excluding the __jac_gen__ folder.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
source_path (str): Path to the action directory.
|
193
|
+
output_filename (str): Desired name of the output .tgz file.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
str: Path to the .tgz file.
|
197
|
+
"""
|
198
|
+
with tarfile.open(output_filename, "w:gz") as tar:
|
199
|
+
for root, dirs, files in os.walk(source_path):
|
200
|
+
# Exclude the __jac_gen__ folder
|
201
|
+
if "__jac_gen__" in dirs:
|
202
|
+
dirs.remove("__jac_gen__")
|
203
|
+
if "__pycache__" in dirs:
|
204
|
+
dirs.remove("__pycache__")
|
205
|
+
for file in files:
|
206
|
+
file_path = os.path.join(root, file)
|
207
|
+
arcname = os.path.relpath(file_path, start=source_path)
|
208
|
+
tar.add(file_path, arcname=arcname)
|
209
|
+
return output_filename
|