elan-cli 0.1.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.
- elan_cli/__init__.py +6 -0
- elan_cli/adk.py +197 -0
- elan_cli/auth.py +391 -0
- elan_cli/client.py +115 -0
- elan_cli/config.py +139 -0
- elan_cli/deploy.py +247 -0
- elan_cli/destroy.py +77 -0
- elan_cli/doctor.py +63 -0
- elan_cli/env.py +96 -0
- elan_cli/files.py +166 -0
- elan_cli/invoke.py +196 -0
- elan_cli/logs.py +149 -0
- elan_cli/main.py +664 -0
- elan_cli/manifests.py +124 -0
- elan_cli/project.py +137 -0
- elan_cli/projects.py +72 -0
- elan_cli/rollback.py +68 -0
- elan_cli/scaffold.py +408 -0
- elan_cli/secret.py +105 -0
- elan_cli/stream.py +162 -0
- elan_cli/tenant.py +59 -0
- elan_cli/validate.py +91 -0
- elan_cli/wallet.py +213 -0
- elan_cli-0.1.0.dist-info/METADATA +96 -0
- elan_cli-0.1.0.dist-info/RECORD +28 -0
- elan_cli-0.1.0.dist-info/WHEEL +4 -0
- elan_cli-0.1.0.dist-info/entry_points.txt +2 -0
- elan_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
elan_cli/tenant.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`elan tenant` — creating a customer, and listing the ones that exist.
|
|
2
|
+
|
|
3
|
+
PLATFORM STAFF ONLY, AND THE CLI DOES NOT CHECK THAT. The service does, and
|
|
4
|
+
saying no in two places means two places to disagree — the one that matters is
|
|
5
|
+
the one holding the Zitadel credential. What this does is make the refusal
|
|
6
|
+
legible: a 403 from the API is printed as the sentence the service wrote, which
|
|
7
|
+
explains that a tenant cannot create another tenant.
|
|
8
|
+
|
|
9
|
+
THIS IS NOT `elan init`. That scaffolds a repo for a customer who already
|
|
10
|
+
exists; this creates the customer. They are adjacent in a developer's day and
|
|
11
|
+
completely different in what they touch, which is why they are separate
|
|
12
|
+
commands rather than flags of one.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from elan_cli.client import Api
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create(api: Api, slug: str, display_name: str = "") -> int:
|
|
21
|
+
"""Create a tenant and print what the person who runs this needs next."""
|
|
22
|
+
body = api.request("POST", "/v1/tenants",
|
|
23
|
+
json={"slug": slug, "display_name": display_name})
|
|
24
|
+
|
|
25
|
+
if not body.get("created", True):
|
|
26
|
+
print(f"tenant {body['tenant']!r} already exists")
|
|
27
|
+
else:
|
|
28
|
+
print(f"created tenant {body['tenant']!r}")
|
|
29
|
+
print(f" organisation {body['org_id']}")
|
|
30
|
+
# THE AUDIENCE. Generated by Zitadel, different in every environment, and
|
|
31
|
+
# unknowable without asking — so it is printed here rather than left to be
|
|
32
|
+
# looked up by whoever wires a client next.
|
|
33
|
+
print(f" audience {body['project_id']}")
|
|
34
|
+
|
|
35
|
+
if body.get("created", True):
|
|
36
|
+
# THE ORG HAS NO USERS, and that is deliberate rather than missing.
|
|
37
|
+
# Creating one means choosing a password or sending an invitation, and
|
|
38
|
+
# both are decisions about who gets into a customer's account. Saying
|
|
39
|
+
# so beats leaving somebody to discover an empty tenant.
|
|
40
|
+
print()
|
|
41
|
+
print(" No users yet — the organisation is empty. Add the customer's")
|
|
42
|
+
print(" first administrator in Zitadel, in the organisation above.")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ls(api: Api) -> int:
|
|
47
|
+
tenants = api.request("GET", "/v1/tenants").get("tenants", [])
|
|
48
|
+
if not tenants:
|
|
49
|
+
print("no tenants")
|
|
50
|
+
return 0
|
|
51
|
+
width = max(len(t["slug"]) for t in tenants)
|
|
52
|
+
for tenant in tenants:
|
|
53
|
+
# FALLS BACK HERE, not in the record. A tenant with no display name is
|
|
54
|
+
# shown by its slug; storing the slug as the name would make a later
|
|
55
|
+
# re-run indistinguishable from somebody naming them that.
|
|
56
|
+
name = tenant.get("display_name") or ""
|
|
57
|
+
suffix = f" {name}" if name and name != tenant["slug"] else ""
|
|
58
|
+
print(f"{tenant['slug']:<{width}} {tenant['created_at'][:10]}{suffix}")
|
|
59
|
+
return 0
|
elan_cli/validate.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""`elan validate` — say no here, where saying no is free.
|
|
2
|
+
|
|
3
|
+
THE SAME MODEL THE SERVICE USES, imported rather than reimplemented. The point
|
|
4
|
+
of validating locally is that a local yes means the service will also say yes;
|
|
5
|
+
a second copy of the schema would agree until it did not, and a CLI that is
|
|
6
|
+
confidently wrong is worse than one that says nothing.
|
|
7
|
+
|
|
8
|
+
WHAT THIS CATCHES THAT THE SCHEMA ALONE CANNOT: the cross-check. `elan.yaml`
|
|
9
|
+
and the Python MANIFESTs are in a validation relationship rather than a
|
|
10
|
+
duplicated one — the manifest says what the agent asks for, the deploy file says
|
|
11
|
+
what the platform will permit — and neither file can be checked against the
|
|
12
|
+
other without reading both. That needs the repo's own code to run, which is why
|
|
13
|
+
this command does something `POST /v1/validate` cannot do for a repo it has
|
|
14
|
+
never built.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from elan_agentkit_contracts.deploy_spec import Problem, cross_check
|
|
22
|
+
|
|
23
|
+
from elan_cli.manifests import ManifestError, read
|
|
24
|
+
from elan_cli.project import Project, ProjectError, find
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(start: Path | None = None, *, python: str | None = None) -> int:
|
|
28
|
+
try:
|
|
29
|
+
project = find(start)
|
|
30
|
+
except ProjectError as exc:
|
|
31
|
+
print(f"error: {exc}")
|
|
32
|
+
return 2
|
|
33
|
+
|
|
34
|
+
print(f"{project.spec_path}")
|
|
35
|
+
print(f" tenant {project.spec.tenant}")
|
|
36
|
+
print(f" project {project.spec.project}")
|
|
37
|
+
print(f" targets {len(project.spec.targets)} declared")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
manifests = read(project.targets_root, python=python)
|
|
41
|
+
except ManifestError as exc:
|
|
42
|
+
print(f"\nerror: {exc}")
|
|
43
|
+
return 2
|
|
44
|
+
|
|
45
|
+
print(f" found {len(manifests)} in {project.targets_root}")
|
|
46
|
+
|
|
47
|
+
problems = cross_check(project.spec, manifests)
|
|
48
|
+
if not problems:
|
|
49
|
+
print("\nok — this would be accepted.")
|
|
50
|
+
_warn_unenforced(project)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
# EVERY PROBLEM, NOT THE FIRST. A developer fixing one, pushing, and waiting
|
|
54
|
+
# for a build to report the next is how a five-minute edit becomes an
|
|
55
|
+
# afternoon. `cross_check` returns them all for the same reason.
|
|
56
|
+
print(f"\nrefused — {len(problems)} problem(s):\n")
|
|
57
|
+
for problem in problems:
|
|
58
|
+
_print(problem)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _print(problem: Problem) -> None:
|
|
63
|
+
print(f" {problem.where}")
|
|
64
|
+
for line in problem.message.splitlines():
|
|
65
|
+
print(f" {line}")
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _warn_unenforced(project: Project) -> None:
|
|
70
|
+
"""`paths:` is parsed, stored, shown — and NOT enforced.
|
|
71
|
+
|
|
72
|
+
Path-level egress needs TLS interception, which means the platform could
|
|
73
|
+
read a tenant's traffic to third parties. That is a consent decision rather
|
|
74
|
+
than a config flag, so v2 enforces the HOST and says so out loud. "Declared
|
|
75
|
+
but unenforced" silently read as "enforced" is how people get breached.
|
|
76
|
+
"""
|
|
77
|
+
declared = sorted({
|
|
78
|
+
rule.host
|
|
79
|
+
for slug in project.spec.targets
|
|
80
|
+
for rule in (project.spec.resolved(slug).egress or [])
|
|
81
|
+
if rule.paths
|
|
82
|
+
})
|
|
83
|
+
if not declared:
|
|
84
|
+
return
|
|
85
|
+
print()
|
|
86
|
+
print(" note: `paths:` is recorded and shown, but NOT enforced. Only the")
|
|
87
|
+
print(" HOST is. Enforcing a path needs TLS interception, which")
|
|
88
|
+
print(" would let the platform read your traffic to that host — so")
|
|
89
|
+
print(" it is not done silently. Affected:")
|
|
90
|
+
for host in declared:
|
|
91
|
+
print(f" {host}")
|
elan_cli/wallet.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""`elan wallet` — credit a tenant holds for THEIR OWN end users.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS, given the console has a page for it: this is the surface a
|
|
4
|
+
developer automates against. Topping up a user from a support script, seeding
|
|
5
|
+
promo credit in CI, or checking a balance while debugging a refusal are all things
|
|
6
|
+
somebody does from a terminal, and the alternative was `curl` with a hand-built
|
|
7
|
+
bearer. A CLI that mirrors the API is also the fastest way to find out the API is
|
|
8
|
+
wrong — which is exactly how the missing `eu:` join would have been caught.
|
|
9
|
+
|
|
10
|
+
DOLLARS IN, MICRO-USD ON THE WIRE. A person types `5` and means five dollars; the
|
|
11
|
+
ledger counts in micro-USD because a float price is a rounding argument with a
|
|
12
|
+
customer. The conversion is here, in one place, and `--micros` exists for the
|
|
13
|
+
caller who already has the integer and should not have to divide by a million to
|
|
14
|
+
pass it back.
|
|
15
|
+
|
|
16
|
+
NO `--tenant`. Every route derives the tenant from the verified token, so naming
|
|
17
|
+
one would be a value the service ignores — and offering a flag that does nothing
|
|
18
|
+
is worse than not offering it.
|
|
19
|
+
|
|
20
|
+
THE PROJECT IS NOT OPTIONAL BUT IS USUALLY IMPLICIT. A wallet is keyed on
|
|
21
|
+
`(tenant, project, external_user_id)` because a tenant runs several products and
|
|
22
|
+
`ada` in one is not `ada` in another. Run inside a project and `elan.yaml` answers
|
|
23
|
+
it, the way `elan deploy` already works; `--project` is for a script that is not
|
|
24
|
+
standing in one.
|
|
25
|
+
|
|
26
|
+
WHAT THIS DELIBERATELY CANNOT DO: close a wallet. Closing needs a zero balance and
|
|
27
|
+
is the one action here that is not meant to be reversed, so it stays in the console
|
|
28
|
+
where the balance is on screen beside the button. `freeze` covers the urgent case.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from elan_cli.client import Api
|
|
34
|
+
from elan_cli.project import ProjectError, find
|
|
35
|
+
|
|
36
|
+
#: Micro-USD in one dollar.
|
|
37
|
+
MICROS = 1_000_000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WalletError(Exception):
|
|
41
|
+
"""Phrased for the developer running the command."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _base(project: str) -> str:
|
|
45
|
+
"""Every wallet route, under its project. One place, so no command can drop
|
|
46
|
+
half the key."""
|
|
47
|
+
return f"/v1/projects/{project}/wallets"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def project_of(explicit: str) -> str:
|
|
51
|
+
"""The project to act in: what was asked for, or what `elan.yaml` says.
|
|
52
|
+
|
|
53
|
+
THE LOCAL FILE RATHER THAN A REMEMBERED DEFAULT. `elan deploy` resolves a
|
|
54
|
+
project the same way, so standing in a directory means the same thing to both
|
|
55
|
+
— and a remembered one would make `elan wallet credit ada 5` act on whichever
|
|
56
|
+
project was touched last, which is the shape of a mistake somebody only finds
|
|
57
|
+
in their statement.
|
|
58
|
+
"""
|
|
59
|
+
if explicit.strip():
|
|
60
|
+
return explicit.strip()
|
|
61
|
+
try:
|
|
62
|
+
return find().spec.project
|
|
63
|
+
except ProjectError as exc:
|
|
64
|
+
raise WalletError(
|
|
65
|
+
"which project? run this inside one, or pass --project "
|
|
66
|
+
f"({exc})") from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _micros(dollars: str | None, micros: int | None) -> int:
|
|
70
|
+
"""The amount, in micro-USD, from whichever form the caller used.
|
|
71
|
+
|
|
72
|
+
ROUNDED RATHER THAN TRUNCATED. `1.1 * 1_000_000` is 1100000.0000001 in
|
|
73
|
+
binary floating point, and `int()` on that credits a user one micro-USD less
|
|
74
|
+
than they were promised — which nobody would ever find, and which makes a
|
|
75
|
+
statement that does not add up.
|
|
76
|
+
"""
|
|
77
|
+
if micros is not None:
|
|
78
|
+
return micros
|
|
79
|
+
if dollars is None:
|
|
80
|
+
raise WalletError("give an amount in dollars, or --micros")
|
|
81
|
+
try:
|
|
82
|
+
return round(float(dollars) * MICROS)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
raise WalletError(f"{dollars!r} is not an amount in dollars") from exc
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _money(micros: int) -> str:
|
|
88
|
+
"""Micro-USD as dollars, without rounding a real charge away.
|
|
89
|
+
|
|
90
|
+
The same rule the console's `money()` follows: a charge of 1,477 micro-USD is
|
|
91
|
+
`$0.0015` and not `$0.00`, because a table of charges that reads as all zeroes
|
|
92
|
+
describes charges that were genuinely made.
|
|
93
|
+
"""
|
|
94
|
+
major = micros / MICROS
|
|
95
|
+
if major == 0:
|
|
96
|
+
return "$0.00"
|
|
97
|
+
# THE SIGN GOES BEFORE THE SYMBOL. `$-2.50` is not how money is written, and a
|
|
98
|
+
# statement is mostly negatives — every run spends.
|
|
99
|
+
sign = "-" if major < 0 else ""
|
|
100
|
+
size = abs(major)
|
|
101
|
+
if size >= 0.01:
|
|
102
|
+
return f"{sign}${size:,.2f}"
|
|
103
|
+
return f"{sign}${size:.4f}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ls(api: Api, project: str, limit: int = 50) -> int:
|
|
107
|
+
page = api.request("GET", f"{_base(project)}?limit={limit}")
|
|
108
|
+
wallets = page.get("wallets", [])
|
|
109
|
+
if not wallets:
|
|
110
|
+
print("no wallets yet — `elan wallet create <user>` makes one")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
width = max(len(w["external_user_id"]) for w in wallets)
|
|
114
|
+
for wallet in wallets:
|
|
115
|
+
held = wallet.get("held_micros") or 0
|
|
116
|
+
# HELD IS SHOWN ONLY WHEN THERE IS SOME, because it explains a balance
|
|
117
|
+
# that looks wrong — a run in flight has reserved its worst case and has
|
|
118
|
+
# not been charged yet — and is noise on every other row.
|
|
119
|
+
extra = f" ({_money(held)} held)" if held else ""
|
|
120
|
+
state = wallet.get("status", "")
|
|
121
|
+
label = f" {wallet['display_label']}" if wallet.get("display_label") else ""
|
|
122
|
+
print(f"{wallet['external_user_id']:<{width}} "
|
|
123
|
+
f"{_money(wallet.get('available_micros', 0)):>12} "
|
|
124
|
+
f"{state:<7}{extra}{label}")
|
|
125
|
+
|
|
126
|
+
total = page.get("total", len(wallets))
|
|
127
|
+
if total > len(wallets):
|
|
128
|
+
print(f"\n{len(wallets)} of {total}; --limit for more")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def show(api: Api, project: str, user: str) -> int:
|
|
133
|
+
wallet = api.request("GET", f"{_base(project)}/{user}")
|
|
134
|
+
print(f"user {wallet['external_user_id']}")
|
|
135
|
+
if wallet.get("display_label"):
|
|
136
|
+
print(f"label {wallet['display_label']}")
|
|
137
|
+
print(f"status {wallet.get('status', '')}")
|
|
138
|
+
print(f"available {_money(wallet.get('available_micros', 0))}")
|
|
139
|
+
if wallet.get("held_micros"):
|
|
140
|
+
print(f"held {_money(wallet['held_micros'])}"
|
|
141
|
+
" (reserved by a run in flight, not yet charged)")
|
|
142
|
+
print(f"balance {_money(wallet.get('balance_micros', 0))}")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create(api: Api, project: str, user: str, label: str = "") -> int:
|
|
147
|
+
wallet = api.request("POST", _base(project),
|
|
148
|
+
json={"external_user_id": user, "display_label": label})
|
|
149
|
+
# IDEMPOTENT, SO THIS DOES NOT CLAIM TO HAVE CREATED ANYTHING. Running it on
|
|
150
|
+
# every sign-in is the expected pattern, and "created" on the fortieth call
|
|
151
|
+
# would be a lie somebody eventually relies on.
|
|
152
|
+
print(f"{wallet['external_user_id']} "
|
|
153
|
+
f"{_money(wallet.get('available_micros', 0))} "
|
|
154
|
+
f"{wallet.get('status', '')}")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def credit(api: Api, project: str, user: str, dollars: str | None,
|
|
159
|
+
micros: int | None, ref: str = "") -> int:
|
|
160
|
+
amount = _micros(dollars, micros)
|
|
161
|
+
if amount <= 0:
|
|
162
|
+
raise WalletError("a credit must be positive; use `adjust` to take credit away")
|
|
163
|
+
done = api.request("POST", f"{_base(project)}/{user}/credits",
|
|
164
|
+
json={"micros": amount, "ref": ref or None})
|
|
165
|
+
print(f"credited {_money(amount)} to {user} {done.get('transaction_id', '')}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def adjust(api: Api, project: str, user: str, dollars: str | None,
|
|
170
|
+
micros: int | None, ref: str = "") -> int:
|
|
171
|
+
amount = _micros(dollars, micros)
|
|
172
|
+
if amount == 0:
|
|
173
|
+
raise WalletError("an adjustment of zero records nothing")
|
|
174
|
+
done = api.request("POST", f"{_base(project)}/{user}/adjustments",
|
|
175
|
+
json={"micros": amount, "ref": ref or None})
|
|
176
|
+
verb = "credited" if amount > 0 else "clawed back"
|
|
177
|
+
print(f"{verb} {_money(abs(amount))} on {user} "
|
|
178
|
+
f"{done.get('transaction_id', '')}")
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def status(api: Api, project: str, user: str, state: str) -> int:
|
|
183
|
+
wallet = api.request("POST", f"{_base(project)}/{user}/status",
|
|
184
|
+
json={"status": state})
|
|
185
|
+
if state == "frozen":
|
|
186
|
+
# SAYS WHAT THE END USER WILL SEE, because that is the question somebody
|
|
187
|
+
# freezing an account is about to be asked.
|
|
188
|
+
print(f"{user} is frozen; their balance is untouched and runs are refused "
|
|
189
|
+
"as unavailable rather than out of credit")
|
|
190
|
+
else:
|
|
191
|
+
print(f"{user} is {wallet.get('status', state)}")
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def statement(api: Api, project: str, user: str, limit: int = 25) -> int:
|
|
196
|
+
page = api.request("GET",
|
|
197
|
+
f"{_base(project)}/{user}/statement?limit={limit}")
|
|
198
|
+
entries = page.get("entries", [])
|
|
199
|
+
if not entries:
|
|
200
|
+
print("nothing has moved on this wallet yet")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
words = {"grant": "credit", "settle": "run", "adjustment": "correction"}
|
|
204
|
+
for entry in entries:
|
|
205
|
+
kind = words.get(entry.get("kind", ""), entry.get("kind", ""))
|
|
206
|
+
ref = f" {entry['ref']}" if entry.get("ref") else ""
|
|
207
|
+
print(f"{entry.get('created_at', '')[:19]} {kind:<11} "
|
|
208
|
+
f"{_money(entry.get('amount_micros', 0)):>12}{ref}")
|
|
209
|
+
|
|
210
|
+
total = page.get("total", len(entries))
|
|
211
|
+
if total > len(entries):
|
|
212
|
+
print(f"\n{len(entries)} of {total}; --limit for more")
|
|
213
|
+
return 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: elan-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The command line a developer deploys an agent with
|
|
5
|
+
Project-URL: Homepage, https://openelan.ai
|
|
6
|
+
Project-URL: Documentation, https://openelan.ai/app/docs
|
|
7
|
+
Author-email: Elan <support@openelan.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: adk,agents,cli,deployment,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: certifi>=2024.2.2
|
|
19
|
+
Requires-Dist: elan-agentkit-contracts
|
|
20
|
+
Requires-Dist: elan-sdk
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: keyring>=25.0
|
|
23
|
+
Requires-Dist: websockets>=13.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# elan-cli
|
|
27
|
+
|
|
28
|
+
The command line a developer deploys an agent with. Installs the `elan`
|
|
29
|
+
command.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install elan-cli # or: pipx install elan-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.12 or newer.
|
|
36
|
+
|
|
37
|
+
## Getting started
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
elan login # device-code sign-in against the platform
|
|
41
|
+
elan init --tenant acme --project agents
|
|
42
|
+
elan env # build the project's venv
|
|
43
|
+
elan validate # say no here, where saying no is free
|
|
44
|
+
elan deploy
|
|
45
|
+
elan invoke invoice-triage --text "What do I owe on INV-9?"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`elan login` reaches the production API by default. Point it elsewhere with
|
|
49
|
+
`elan login --api https://…`, or set `ELAN_API`.
|
|
50
|
+
|
|
51
|
+
## What it covers
|
|
52
|
+
|
|
53
|
+
| Group | Commands |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Signing in | `login`, `logout`, `whoami` |
|
|
56
|
+
| Project setup | `init`, `new {agent,workflow}`, `projects {ls,create}` |
|
|
57
|
+
| Local development | `env`, `adk`, `validate` |
|
|
58
|
+
| Shipping | `deploy`, `status`, `logs`, `rollback` |
|
|
59
|
+
| Operating | `agents`, `invoke`, `doctor`, `destroy` |
|
|
60
|
+
| Secrets | `secret {set,ls,rm}` |
|
|
61
|
+
| Files | `files {ls,get,put,rm}` |
|
|
62
|
+
| Wallets | `wallet {ls,show,create,credit,adjust,freeze,unfreeze,statement}` |
|
|
63
|
+
|
|
64
|
+
`elan <command> --help` for the flags.
|
|
65
|
+
|
|
66
|
+
## Where it keeps things
|
|
67
|
+
|
|
68
|
+
Configuration is a plain JSON file at `~/.config/elan/config.json`, mode
|
|
69
|
+
`0600`, holding only public values — the API URL, the issuer, the client id.
|
|
70
|
+
Override the directory with `ELAN_HOME`, or the API with `ELAN_API`.
|
|
71
|
+
|
|
72
|
+
**Credentials never go in a file.** The refresh token is stored in the OS
|
|
73
|
+
keyring under the service name `elan-cli`. The CLI will not fall back to
|
|
74
|
+
writing one to disk; if `keyring` cannot be reached it says so and stops.
|
|
75
|
+
|
|
76
|
+
Per-project state lives in `.elan/` beside your `elan.yaml`, and is
|
|
77
|
+
git-ignored.
|
|
78
|
+
|
|
79
|
+
## Exit codes
|
|
80
|
+
|
|
81
|
+
| | |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `0` | it worked |
|
|
84
|
+
| `1` | it ran, and the answer was no — a refused deploy, a failed validation |
|
|
85
|
+
| `2` | it could not run — no project, no credential, no network |
|
|
86
|
+
|
|
87
|
+
Errors print as `error: …` on stderr with no traceback.
|
|
88
|
+
|
|
89
|
+
## Documentation
|
|
90
|
+
|
|
91
|
+
The full command reference and the platform guides are at
|
|
92
|
+
<https://openelan.ai/app/docs>.
|
|
93
|
+
|
|
94
|
+
## Licence
|
|
95
|
+
|
|
96
|
+
Apache-2.0. See `LICENSE`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
elan_cli/__init__.py,sha256=oTkVmV-vxUHF0Ew_TZXeUj4WhIZSc5FmrZUaBzIBgXY,220
|
|
2
|
+
elan_cli/adk.py,sha256=Wim59nEppwGWRTzXIvDWATUivByiphiIAuhhktXur5o,8144
|
|
3
|
+
elan_cli/auth.py,sha256=a9v2mQZGKzHPiWRz-HL8jSVN1oPcJUAfDTU8lxQvvSk,15614
|
|
4
|
+
elan_cli/client.py,sha256=Br-YdThu816lSwoIpYklb3hujv47lNARSXveym_1FmI,4862
|
|
5
|
+
elan_cli/config.py,sha256=RqSOFh_TK_5Bi6AqOQZPada90FAtS7gclJvOn_tBlkI,6109
|
|
6
|
+
elan_cli/deploy.py,sha256=UTe7SIQkMMP5eudRDwup6t3GnaRKtzKAJfxEuDzSCMk,9751
|
|
7
|
+
elan_cli/destroy.py,sha256=zQlb0pkFiD79bwq3TX5JSgkNS9sc14nRLBDaJrgcKYE,3180
|
|
8
|
+
elan_cli/doctor.py,sha256=zXB-CC7tCNQlX8ktkgD5ghGLwjcgQ-QhbgF1L_O6fGk,2535
|
|
9
|
+
elan_cli/env.py,sha256=53mNrnm7RrG3Q7tgWt0ARTMoa58xZ8i1n0i621nbOJE,4008
|
|
10
|
+
elan_cli/files.py,sha256=AIv3i-6yNhQ_1sgoMdgq1Lv0ecPQirUKM3OXCXLEkNk,6302
|
|
11
|
+
elan_cli/invoke.py,sha256=SsaHWrWRJoirfkKsvkqoZX-aiJjTYIFsq1jyiS4l_6Q,9394
|
|
12
|
+
elan_cli/logs.py,sha256=qpiqnipK8MgeTishyOWzJx7hs53ZmwstivCFEyx-9ws,6171
|
|
13
|
+
elan_cli/main.py,sha256=67ZBBWBggvK1vwf5GcZ4qTxt_hyExXtCnN_QYLmMawY,29499
|
|
14
|
+
elan_cli/manifests.py,sha256=lziJcrVn0qib70rpbSf-Lm0HSF_kUI0HgeMQluszOlw,5786
|
|
15
|
+
elan_cli/project.py,sha256=v7Vp4dw7JWesf70K2fJ0bMJwEJMqsLqtPWP1z0N-TFk,5662
|
|
16
|
+
elan_cli/projects.py,sha256=EeWdJ5BBE0sncOSgbZMxEZug8l5hpUu46EmAOpONOWw,2656
|
|
17
|
+
elan_cli/rollback.py,sha256=-wjAU6ZM4vDg_Rd3uHbrswShHW6ZZkVXCPxlZm4t7ho,2740
|
|
18
|
+
elan_cli/scaffold.py,sha256=6dEwaCTMg7riyydpFzEADsGIa6hjKhaOSBTpac9Ql2I,17094
|
|
19
|
+
elan_cli/secret.py,sha256=Y3sp866TPRsBzE2Iu4Ncw1mAQk3TtEv1FHEdmHPN7t8,4311
|
|
20
|
+
elan_cli/stream.py,sha256=ESvMHNivLUS3Q_Mzrm1TjCk9fXFcfXNm30PizamhKP4,7589
|
|
21
|
+
elan_cli/tenant.py,sha256=HjYvxyXlAwyGBClzPyH7nAQrGYHWCY1WLV3HXNV0Ta8,2697
|
|
22
|
+
elan_cli/validate.py,sha256=R3HIm5CFasHBprcc85XCCaFwN4oXuG4lWOoHu1ry8Rg,3383
|
|
23
|
+
elan_cli/wallet.py,sha256=YVXoqtO1IaLaubPccf_0TrV4rmNnIDZmx1c-Py-DrVw,9032
|
|
24
|
+
elan_cli-0.1.0.dist-info/METADATA,sha256=6fvz2T4RtX-E3X6cPjbHfzJWrNH03osoSBALVhL9xxQ,3066
|
|
25
|
+
elan_cli-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
26
|
+
elan_cli-0.1.0.dist-info/entry_points.txt,sha256=nhqf7kifhbxc2pEurRsqWIr_GgA2FwfQQViKfw5fUUs,43
|
|
27
|
+
elan_cli-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
28
|
+
elan_cli-0.1.0.dist-info/RECORD,,
|