certmate-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.
certmate_cli/__init__.py
ADDED
certmate_cli/main.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""CertMate CLI — a thin, pleasant terminal front-end over certmate-sdk.
|
|
2
|
+
|
|
3
|
+
certmate cert create app.example.com --dns cloudflare --wait
|
|
4
|
+
certmate cert ls
|
|
5
|
+
certmate cert renew app.example.com --force
|
|
6
|
+
certmate audit verify
|
|
7
|
+
|
|
8
|
+
Connection comes from --url/--token or CERTMATE_URL/CERTMATE_TOKEN.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from certmate import Client, CertMateError, Job
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(no_args_is_help=True, add_completion=False,
|
|
22
|
+
help="CertMate — the SSL certificate lifecycle from your terminal.")
|
|
23
|
+
cert_app = typer.Typer(no_args_is_help=True, help="Manage certificates.")
|
|
24
|
+
dns_app = typer.Typer(no_args_is_help=True, help="DNS providers and accounts.")
|
|
25
|
+
audit_app = typer.Typer(no_args_is_help=True, help="Tamper-evident audit trail.")
|
|
26
|
+
backup_app = typer.Typer(no_args_is_help=True, help="Backups.")
|
|
27
|
+
deploy_app = typer.Typer(no_args_is_help=True, help="Post-issuance deploy hooks.")
|
|
28
|
+
app.add_typer(cert_app, name="cert")
|
|
29
|
+
app.add_typer(dns_app, name="dns")
|
|
30
|
+
app.add_typer(audit_app, name="audit")
|
|
31
|
+
app.add_typer(backup_app, name="backup")
|
|
32
|
+
app.add_typer(deploy_app, name="deploy")
|
|
33
|
+
|
|
34
|
+
out = Console()
|
|
35
|
+
err = Console(stderr=True)
|
|
36
|
+
|
|
37
|
+
# A lax hostname/wildcard check for the client-side --dry-run preflight; the
|
|
38
|
+
# server is the real authority, this just catches obvious typos before we
|
|
39
|
+
# spend an API call.
|
|
40
|
+
_DOMAIN_RE = re.compile(
|
|
41
|
+
r"^(\*\.)?([a-zA-Z0-9_](-*[a-zA-Z0-9_])*\.)+[a-zA-Z]{2,}$")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.callback()
|
|
45
|
+
def _main(
|
|
46
|
+
ctx: typer.Context,
|
|
47
|
+
url: Optional[str] = typer.Option(None, "--url", envvar="CERTMATE_URL",
|
|
48
|
+
help="CertMate base URL (default http://localhost:8000)."),
|
|
49
|
+
token: Optional[str] = typer.Option(None, "--token", envvar="CERTMATE_TOKEN",
|
|
50
|
+
help="API bearer token."),
|
|
51
|
+
):
|
|
52
|
+
ctx.obj = {"url": url, "token": token}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _client(ctx: typer.Context) -> Client:
|
|
56
|
+
o = ctx.obj or {}
|
|
57
|
+
return Client(o.get("url"), o.get("token"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _die(msg: str, code: int = 1):
|
|
61
|
+
err.print(f"[bold red]error[/]: {msg}")
|
|
62
|
+
raise typer.Exit(code)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run(fn):
|
|
66
|
+
"""Execute an SDK call, turning SDK errors into clean CLI failures."""
|
|
67
|
+
try:
|
|
68
|
+
return fn()
|
|
69
|
+
except CertMateError as e:
|
|
70
|
+
_die(str(e))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _records(data) -> List[dict]:
|
|
74
|
+
"""Coerce a list/dict API response into a list of record dicts."""
|
|
75
|
+
if isinstance(data, list):
|
|
76
|
+
return [d for d in data if isinstance(d, dict)]
|
|
77
|
+
if isinstance(data, dict):
|
|
78
|
+
for key in ("items", "accounts", "backups", "results", "data", "unified"):
|
|
79
|
+
v = data.get(key)
|
|
80
|
+
if isinstance(v, list):
|
|
81
|
+
return [d for d in v if isinstance(d, dict)]
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _table(records: List[dict], columns: List[str]) -> None:
|
|
86
|
+
"""Print a rich table of ``records`` over ``columns`` (missing keys -> '-')."""
|
|
87
|
+
if not records:
|
|
88
|
+
out.print("[dim]nothing to show.[/]")
|
|
89
|
+
return
|
|
90
|
+
table = Table(box=None, header_style="bold")
|
|
91
|
+
for c in columns:
|
|
92
|
+
table.add_column(c.upper())
|
|
93
|
+
for r in records:
|
|
94
|
+
table.add_row(*[str(r.get(c, "-")) for c in columns])
|
|
95
|
+
out.print(table)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------------
|
|
99
|
+
# cert
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
@cert_app.command("ls")
|
|
103
|
+
def cert_ls(ctx: typer.Context):
|
|
104
|
+
"""List certificates with expiry and status."""
|
|
105
|
+
certs = _run(lambda: _client(ctx).list_certificates())
|
|
106
|
+
if not certs:
|
|
107
|
+
out.print("[dim]No certificates.[/]")
|
|
108
|
+
return
|
|
109
|
+
table = Table(box=None, header_style="bold")
|
|
110
|
+
table.add_column("DOMAIN")
|
|
111
|
+
table.add_column("EXPIRES")
|
|
112
|
+
table.add_column("DAYS", justify="right")
|
|
113
|
+
table.add_column("CA")
|
|
114
|
+
table.add_column("AUTO-RENEW")
|
|
115
|
+
for c in sorted(certs, key=lambda x: (x.days_until_expiry if x.days_until_expiry is not None else 1 << 30)):
|
|
116
|
+
days = c.days_until_expiry
|
|
117
|
+
days_str = "-" if days is None else str(days)
|
|
118
|
+
colour = "green"
|
|
119
|
+
if days is not None:
|
|
120
|
+
colour = "red" if days < 7 else ("yellow" if days < 30 else "green")
|
|
121
|
+
table.add_row(
|
|
122
|
+
c.domain,
|
|
123
|
+
(c.expiry_date or "-")[:10],
|
|
124
|
+
f"[{colour}]{days_str}[/]",
|
|
125
|
+
c.ca_provider or "-",
|
|
126
|
+
"on" if c.auto_renew else ("off" if c.auto_renew is not None else "-"),
|
|
127
|
+
)
|
|
128
|
+
out.print(table)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@cert_app.command("info")
|
|
132
|
+
def cert_info(ctx: typer.Context, domain: str):
|
|
133
|
+
"""Show a certificate's details."""
|
|
134
|
+
c = _run(lambda: _client(ctx).get_certificate(domain))
|
|
135
|
+
out.print(f"[bold]{c.domain}[/]")
|
|
136
|
+
out.print(f" expires: {c.expiry_date or '-'} "
|
|
137
|
+
f"({c.days_until_expiry if c.days_until_expiry is not None else '?'} days)")
|
|
138
|
+
out.print(f" CA: {c.ca_provider or '-'}")
|
|
139
|
+
out.print(f" DNS: {c.dns_provider or '-'}")
|
|
140
|
+
out.print(f" auto-renew: {c.auto_renew}")
|
|
141
|
+
if c.san_domains:
|
|
142
|
+
out.print(f" SAN: {', '.join(c.san_domains)}")
|
|
143
|
+
if c.needs_renewal:
|
|
144
|
+
out.print(" [yellow]needs renewal[/]")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cert_app.command("create")
|
|
148
|
+
def cert_create(
|
|
149
|
+
ctx: typer.Context,
|
|
150
|
+
domain: str,
|
|
151
|
+
dns: Optional[str] = typer.Option(None, "--dns", help="DNS provider (e.g. cloudflare)."),
|
|
152
|
+
ca: Optional[str] = typer.Option(None, "--ca", help="CA provider (e.g. letsencrypt)."),
|
|
153
|
+
san: Optional[str] = typer.Option(None, "--san", help="Comma-separated SAN domains."),
|
|
154
|
+
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for issuance to finish."),
|
|
155
|
+
dry_run: bool = typer.Option(False, "--dry-run",
|
|
156
|
+
help="Validate inputs and preflight the DNS provider WITHOUT issuing."),
|
|
157
|
+
):
|
|
158
|
+
"""Issue a certificate (async; waits for completion by default)."""
|
|
159
|
+
sans: List[str] = [s.strip() for s in san.split(",")] if san else []
|
|
160
|
+
client = _client(ctx)
|
|
161
|
+
|
|
162
|
+
if dry_run:
|
|
163
|
+
problems = []
|
|
164
|
+
if not _DOMAIN_RE.match(domain):
|
|
165
|
+
problems.append(f"domain {domain!r} does not look valid")
|
|
166
|
+
for s in sans:
|
|
167
|
+
if not _DOMAIN_RE.match(s):
|
|
168
|
+
problems.append(f"SAN {s!r} does not look valid")
|
|
169
|
+
if dns:
|
|
170
|
+
res = _run(lambda: client.test_dns_provider(dns))
|
|
171
|
+
ok = bool(res.get("success", res.get("ok", True)))
|
|
172
|
+
msg = res.get("message") or res.get("error") or ("reachable" if ok else "failed")
|
|
173
|
+
out.print(f"DNS provider [bold]{dns}[/]: {'[green]OK[/]' if ok else '[red]FAIL[/]'} — {msg}")
|
|
174
|
+
if not ok:
|
|
175
|
+
problems.append("DNS provider preflight failed")
|
|
176
|
+
else:
|
|
177
|
+
out.print("[dim]No --dns given; skipping provider preflight.[/]")
|
|
178
|
+
if problems:
|
|
179
|
+
_die("dry-run found issues:\n - " + "\n - ".join(problems))
|
|
180
|
+
out.print(f"[green]dry-run OK[/] — would issue [bold]{domain}[/]"
|
|
181
|
+
f"{(' (+' + str(len(sans)) + ' SAN)') if sans else ''}"
|
|
182
|
+
f"{(' via ' + dns) if dns else ''}{(' on ' + ca) if ca else ''}. Nothing issued.")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
def _issue():
|
|
186
|
+
with out.status(f"Issuing [bold]{domain}[/] …") as status:
|
|
187
|
+
def _progress(j: Job):
|
|
188
|
+
if j.status:
|
|
189
|
+
status.update(f"Issuing [bold]{domain}[/] … [{j.status}]")
|
|
190
|
+
return client.create_certificate(
|
|
191
|
+
domain, dns_provider=dns, ca_provider=ca, san_domains=sans or None,
|
|
192
|
+
wait=wait, on_progress=_progress)
|
|
193
|
+
|
|
194
|
+
job = _run(_issue)
|
|
195
|
+
if wait:
|
|
196
|
+
out.print(f"[green]issued[/] [bold]{domain}[/]"
|
|
197
|
+
f"{(' (' + job.status + ')') if job.status else ''}.")
|
|
198
|
+
else:
|
|
199
|
+
out.print(f"[yellow]accepted[/] — job [bold]{job.job_id}[/] "
|
|
200
|
+
f"(poll: certmate cert job {job.job_id}).")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@cert_app.command("job")
|
|
204
|
+
def cert_job(ctx: typer.Context, job_id: str):
|
|
205
|
+
"""Show the status of an async issuance/renewal job."""
|
|
206
|
+
j = _run(lambda: _client(ctx).get_job(job_id))
|
|
207
|
+
out.print(f"job [bold]{j.job_id}[/]: {j.status or '?'}"
|
|
208
|
+
f"{(' — ' + j.error) if j.error else ''}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@cert_app.command("renew")
|
|
212
|
+
def cert_renew(ctx: typer.Context, domain: str,
|
|
213
|
+
force: bool = typer.Option(False, "--force", help="Force renewal even if not due.")):
|
|
214
|
+
"""Renew a certificate."""
|
|
215
|
+
res = _run(lambda: _client(ctx).renew_certificate(domain, force=force))
|
|
216
|
+
if res.get("renewed") is False:
|
|
217
|
+
out.print(f"[yellow]not due[/] — {domain} was not yet due for renewal.")
|
|
218
|
+
else:
|
|
219
|
+
out.print(f"[green]renewed[/] {domain}.")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@cert_app.command("rm")
|
|
223
|
+
def cert_rm(ctx: typer.Context, domain: str,
|
|
224
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation.")):
|
|
225
|
+
"""Delete a certificate."""
|
|
226
|
+
if not yes:
|
|
227
|
+
typer.confirm(f"Delete certificate {domain}?", abort=True)
|
|
228
|
+
_run(lambda: _client(ctx).delete_certificate(domain))
|
|
229
|
+
out.print(f"[green]deleted[/] {domain}.")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@cert_app.command("reissue")
|
|
233
|
+
def cert_reissue(ctx: typer.Context, domain: str,
|
|
234
|
+
san: Optional[str] = typer.Option(None, "--san", help="Comma-separated SAN domains.")):
|
|
235
|
+
"""Reissue a certificate (e.g. to change its SAN list)."""
|
|
236
|
+
body = {}
|
|
237
|
+
if san is not None:
|
|
238
|
+
body["san_domains"] = [s.strip() for s in san.split(",") if s.strip()]
|
|
239
|
+
_run(lambda: _client(ctx).reissue_certificate(domain, **body))
|
|
240
|
+
out.print(f"[green]reissued[/] {domain}.")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# --------------------------------------------------------------------------
|
|
244
|
+
# dns
|
|
245
|
+
# --------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
@dns_app.command("providers")
|
|
248
|
+
def dns_providers(ctx: typer.Context):
|
|
249
|
+
"""List supported DNS providers."""
|
|
250
|
+
out.print(_run(lambda: _client(ctx).list_dns_providers()))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dns_app.command("accounts")
|
|
254
|
+
def dns_accounts(ctx: typer.Context,
|
|
255
|
+
provider: Optional[str] = typer.Argument(None, help="Filter by provider.")):
|
|
256
|
+
"""List configured DNS accounts."""
|
|
257
|
+
data = _run(lambda: _client(ctx).list_dns_accounts(provider))
|
|
258
|
+
_table(_records(data), ["provider", "account_id", "name", "email"])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dns_app.command("test")
|
|
262
|
+
def dns_test(ctx: typer.Context, provider: str):
|
|
263
|
+
"""Preflight a DNS provider (does not issue)."""
|
|
264
|
+
res = _run(lambda: _client(ctx).test_dns_provider(provider))
|
|
265
|
+
ok = bool(res.get("success", res.get("ok", True)))
|
|
266
|
+
msg = res.get("message") or res.get("error") or ("reachable" if ok else "failed")
|
|
267
|
+
out.print(f"[bold]{provider}[/]: {'[green]OK[/]' if ok else '[red]FAIL[/]'} — {msg}")
|
|
268
|
+
if not ok:
|
|
269
|
+
raise typer.Exit(1)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# --------------------------------------------------------------------------
|
|
273
|
+
# audit
|
|
274
|
+
# --------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
@audit_app.command("verify")
|
|
277
|
+
def audit_verify(ctx: typer.Context):
|
|
278
|
+
"""Verify the tamper-evident audit chain."""
|
|
279
|
+
res = _run(lambda: _client(ctx).audit_verify())
|
|
280
|
+
ok = bool(res.get("ok"))
|
|
281
|
+
reason = res.get("reason") or ""
|
|
282
|
+
cp = res.get("checkpoint_verified")
|
|
283
|
+
# "chain file does not exist" / "empty chain" is a fresh instance that has
|
|
284
|
+
# not audited anything yet — benign, not a tamper alarm. Show it neutrally.
|
|
285
|
+
if not ok and any(k in reason.lower() for k in ("does not exist", "empty")):
|
|
286
|
+
out.print(f"audit chain: [dim]none yet[/] — {reason}")
|
|
287
|
+
return
|
|
288
|
+
out.print(f"audit chain: {'[green]intact[/]' if ok else '[red]BROKEN[/]'}"
|
|
289
|
+
f"{(' — ' + reason) if reason else ''}")
|
|
290
|
+
if cp is not None:
|
|
291
|
+
out.print(f" signed checkpoint: {'[green]verified[/]' if cp else '[dim]not cross-checked[/]'}"
|
|
292
|
+
f"{(' @ seq ' + str(res.get('checkpoint_seq'))) if res.get('checkpoint_seq') is not None else ''}")
|
|
293
|
+
if not ok:
|
|
294
|
+
raise typer.Exit(1)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --------------------------------------------------------------------------
|
|
298
|
+
# backup
|
|
299
|
+
# --------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
@backup_app.command("ls")
|
|
302
|
+
def backup_ls(ctx: typer.Context):
|
|
303
|
+
"""List backups."""
|
|
304
|
+
data = _run(lambda: _client(ctx).list_backups())
|
|
305
|
+
# Items are {filename, metadata:{size, created, backup_reason, ...}} — flatten.
|
|
306
|
+
flat = [{**r, **(r.get("metadata") or {})} for r in _records(data)]
|
|
307
|
+
_table(flat, ["filename", "backup_reason", "created", "size"])
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@backup_app.command("create")
|
|
311
|
+
def backup_create(ctx: typer.Context):
|
|
312
|
+
"""Create a backup now."""
|
|
313
|
+
res = _run(lambda: _client(ctx).create_backup())
|
|
314
|
+
name = (res or {}).get("filename") or (res or {}).get("backup") or "ok"
|
|
315
|
+
out.print(f"[green]backup created[/] {name}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# --------------------------------------------------------------------------
|
|
319
|
+
# deploy
|
|
320
|
+
# --------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
@deploy_app.command("run")
|
|
323
|
+
def deploy_run(ctx: typer.Context, domain: str):
|
|
324
|
+
"""Run the configured deploy hooks for a domain now."""
|
|
325
|
+
res = _run(lambda: _client(ctx).deploy_certificate(domain))
|
|
326
|
+
ok = bool((res or {}).get("ok", True))
|
|
327
|
+
out.print(f"deploy [bold]{domain}[/]: {'[green]ok[/]' if ok else '[red]failed[/]'}"
|
|
328
|
+
f"{(' — ' + str(res.get('message'))) if isinstance(res, dict) and res.get('message') else ''}")
|
|
329
|
+
if not ok:
|
|
330
|
+
raise typer.Exit(1)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# --------------------------------------------------------------------------
|
|
334
|
+
# top-level convenience
|
|
335
|
+
# --------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
@app.command("health")
|
|
338
|
+
def health(ctx: typer.Context):
|
|
339
|
+
"""Check the CertMate instance health."""
|
|
340
|
+
out.print(_run(lambda: _client(ctx).health()))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
app()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certmate-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CertMate command-line interface — the SSL certificate lifecycle from your terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/fabriziosalmi/certmate
|
|
6
|
+
Project-URL: Source, https://github.com/fabriziosalmi/certmate/tree/main/clients/certmate-cli
|
|
7
|
+
Author-email: Fabrizio Salmi <fabrizio.salmi@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: acme,certmate,cli,letsencrypt,tls
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: System :: Systems Administration
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: certmate-sdk>=0.1.0
|
|
27
|
+
Requires-Dist: rich>=13
|
|
28
|
+
Requires-Dist: typer>=0.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# certmate-cli
|
|
32
|
+
|
|
33
|
+
The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
|
|
34
|
+
lifecycle from your terminal — built on `certmate-sdk`.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install certmate-cli
|
|
38
|
+
export CERTMATE_URL=http://localhost:8000
|
|
39
|
+
export CERTMATE_TOKEN=...
|
|
40
|
+
|
|
41
|
+
certmate cert create app.example.com --dns cloudflare --wait
|
|
42
|
+
certmate cert ls
|
|
43
|
+
certmate cert info app.example.com
|
|
44
|
+
certmate cert renew app.example.com --force
|
|
45
|
+
certmate cert create app.example.com --dns cloudflare --dry-run
|
|
46
|
+
certmate audit verify
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Connection comes from `--url`/`--token` or `CERTMATE_URL`/`CERTMATE_TOKEN`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
certmate_cli/__init__.py,sha256=ORHTYH92NSZVLV-rzJhdnhpHIefR6r2EjZiDcGIqHG8,116
|
|
2
|
+
certmate_cli/main.py,sha256=2G4pduCAUL748L8PLcny9jtLdeh8iQ4XZLWkCg_bOq4,13645
|
|
3
|
+
certmate_cli-0.1.0.dist-info/METADATA,sha256=GfWUDxXFw0QZ-QyduCFbZKjmtD61VMneGX9dHpNmAYA,1838
|
|
4
|
+
certmate_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
certmate_cli-0.1.0.dist-info/entry_points.txt,sha256=3ArMOEL-UOhkPlq1U5GzdXkA01LehM0tAr6_GFmDlTc,51
|
|
6
|
+
certmate_cli-0.1.0.dist-info/RECORD,,
|