@yukioa2z/visa-apply 3.0.0
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/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/install.js +67 -0
- package/package.json +34 -0
- package/skill/SKILL.md +80 -0
- package/skill/agents/openai.yaml +7 -0
- package/skill/assets/visa-dossier-template.html +601 -0
- package/skill/references/adapter-schema.md +55 -0
- package/skill/references/core-intake.md +66 -0
- package/skill/references/countries/au.md +149 -0
- package/skill/references/countries/ca.md +124 -0
- package/skill/references/countries/jp.md +133 -0
- package/skill/references/countries/schengen.md +183 -0
- package/skill/references/countries/uk.md +144 -0
- package/skill/references/countries/us.md +134 -0
- package/skill/references/jurisdictions.json +1538 -0
- package/skill/references/live-route-check.md +76 -0
- package/skill/references/official-sources.json +1861 -0
- package/skill/references/pipeline.md +104 -0
- package/skill/references/quality-gates.md +52 -0
- package/skill/scripts/create_dossier.py +140 -0
- package/skill/scripts/source_registry.py +386 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate and query the visa destination and official-source registries."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from datetime import date
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
REGISTRY_PATH = ROOT / "references" / "official-sources.json"
|
|
15
|
+
DIRECTORY_PATH = ROOT / "references" / "jurisdictions.json"
|
|
16
|
+
IATA_TRAVEL_CENTRE = (
|
|
17
|
+
"https://www.iata.org/en/services/compliance/timatic/travel-documentation/"
|
|
18
|
+
)
|
|
19
|
+
ALLOWED_LEVELS = {"full_adapter", "source_seeded", "discovered"}
|
|
20
|
+
ALLOWED_ROLES = {
|
|
21
|
+
"authoritative_rules",
|
|
22
|
+
"official_application",
|
|
23
|
+
"delegated_application",
|
|
24
|
+
"official_status",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_registry() -> dict:
|
|
29
|
+
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_directory() -> dict:
|
|
33
|
+
return json.loads(DIRECTORY_PATH.read_text(encoding="utf-8"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _candidates(jurisdiction: dict) -> set[str]:
|
|
37
|
+
values = {
|
|
38
|
+
jurisdiction.get("id", ""),
|
|
39
|
+
jurisdiction.get("iso2", ""),
|
|
40
|
+
jurisdiction.get("name", ""),
|
|
41
|
+
*jurisdiction.get("aliases", []),
|
|
42
|
+
}
|
|
43
|
+
return {value.casefold() for value in values if value}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_jurisdiction(registry: dict, query: str) -> dict | None:
|
|
47
|
+
normalized = query.casefold()
|
|
48
|
+
return next(
|
|
49
|
+
(
|
|
50
|
+
jurisdiction
|
|
51
|
+
for jurisdiction in registry["jurisdictions"]
|
|
52
|
+
if normalized in _candidates(jurisdiction)
|
|
53
|
+
),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_directory_jurisdiction(directory: dict, query: str) -> dict | None:
|
|
59
|
+
normalized = query.casefold()
|
|
60
|
+
return next(
|
|
61
|
+
(
|
|
62
|
+
jurisdiction
|
|
63
|
+
for jurisdiction in directory["jurisdictions"]
|
|
64
|
+
if normalized in _candidates(jurisdiction)
|
|
65
|
+
),
|
|
66
|
+
None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def find_destination(registry: dict, directory: dict, query: str) -> tuple[dict, dict | None] | None:
|
|
71
|
+
seed = find_jurisdiction(registry, query)
|
|
72
|
+
if seed:
|
|
73
|
+
directory_match = (
|
|
74
|
+
find_directory_jurisdiction(directory, seed["iso2"])
|
|
75
|
+
if seed.get("iso2")
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
return directory_match or seed, seed
|
|
79
|
+
|
|
80
|
+
directory_match = find_directory_jurisdiction(directory, query)
|
|
81
|
+
if directory_match:
|
|
82
|
+
seed = find_jurisdiction(registry, directory_match["iso2"])
|
|
83
|
+
return directory_match, seed
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _valid_https_url(value: str) -> bool:
|
|
88
|
+
parsed = urlparse(value)
|
|
89
|
+
return parsed.scheme == "https" and bool(parsed.netloc)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _valid_iso_date(value: str) -> bool:
|
|
93
|
+
if not isinstance(value, str):
|
|
94
|
+
return False
|
|
95
|
+
try:
|
|
96
|
+
date.fromisoformat(value)
|
|
97
|
+
return True
|
|
98
|
+
except ValueError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate(registry: dict, directory: dict | None = None) -> list[str]:
|
|
103
|
+
errors: list[str] = []
|
|
104
|
+
if registry.get("schema_version") != 1:
|
|
105
|
+
errors.append("official source schema_version must be 1")
|
|
106
|
+
if not _valid_iso_date(registry.get("last_verified", "")):
|
|
107
|
+
errors.append("official source last_verified must be an ISO date (YYYY-MM-DD)")
|
|
108
|
+
|
|
109
|
+
jurisdictions = registry.get("jurisdictions")
|
|
110
|
+
if not isinstance(jurisdictions, list) or not jurisdictions:
|
|
111
|
+
return errors + ["official source jurisdictions must be a non-empty list"]
|
|
112
|
+
|
|
113
|
+
seen_ids: set[str] = set()
|
|
114
|
+
for index, jurisdiction in enumerate(jurisdictions):
|
|
115
|
+
prefix = f"official_sources.jurisdictions[{index}]"
|
|
116
|
+
jurisdiction_id = jurisdiction.get("id")
|
|
117
|
+
if not jurisdiction_id:
|
|
118
|
+
errors.append(f"{prefix}.id is required")
|
|
119
|
+
elif jurisdiction_id in seen_ids:
|
|
120
|
+
errors.append(f"duplicate official source jurisdiction id: {jurisdiction_id}")
|
|
121
|
+
else:
|
|
122
|
+
seen_ids.add(jurisdiction_id)
|
|
123
|
+
|
|
124
|
+
if jurisdiction.get("support_level") not in ALLOWED_LEVELS:
|
|
125
|
+
errors.append(f"{prefix}.support_level is invalid")
|
|
126
|
+
|
|
127
|
+
if not _valid_iso_date(jurisdiction.get("last_verified", "")):
|
|
128
|
+
errors.append(f"{prefix}.last_verified must be an ISO date (YYYY-MM-DD)")
|
|
129
|
+
|
|
130
|
+
sources = jurisdiction.get("sources")
|
|
131
|
+
if not isinstance(sources, list) or not sources:
|
|
132
|
+
errors.append(f"{prefix}.sources must be a non-empty list")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
has_rule_authority = False
|
|
136
|
+
for source_index, source in enumerate(sources):
|
|
137
|
+
source_prefix = f"{prefix}.sources[{source_index}]"
|
|
138
|
+
roles = set(source.get("roles", []))
|
|
139
|
+
unknown_roles = roles - ALLOWED_ROLES
|
|
140
|
+
if unknown_roles:
|
|
141
|
+
errors.append(f"{source_prefix}.roles contains {sorted(unknown_roles)}")
|
|
142
|
+
if "authoritative_rules" in roles:
|
|
143
|
+
has_rule_authority = True
|
|
144
|
+
if not _valid_https_url(source.get("url", "")):
|
|
145
|
+
errors.append(f"{source_prefix}.url must be an https URL")
|
|
146
|
+
for key in ("label", "authority", "covers"):
|
|
147
|
+
if not source.get(key):
|
|
148
|
+
errors.append(f"{source_prefix}.{key} is required")
|
|
149
|
+
|
|
150
|
+
if not has_rule_authority:
|
|
151
|
+
errors.append(f"{prefix} needs an authoritative_rules source")
|
|
152
|
+
|
|
153
|
+
if directory is None:
|
|
154
|
+
return errors
|
|
155
|
+
if directory.get("schema_version") != 1:
|
|
156
|
+
errors.append("jurisdiction directory schema_version must be 1")
|
|
157
|
+
|
|
158
|
+
directory_jurisdictions = directory.get("jurisdictions")
|
|
159
|
+
if not isinstance(directory_jurisdictions, list) or not directory_jurisdictions:
|
|
160
|
+
return errors + ["directory jurisdictions must be a non-empty list"]
|
|
161
|
+
|
|
162
|
+
directory_ids: set[str] = set()
|
|
163
|
+
directory_iso2: set[str] = set()
|
|
164
|
+
for index, jurisdiction in enumerate(directory_jurisdictions):
|
|
165
|
+
prefix = f"directory.jurisdictions[{index}]"
|
|
166
|
+
jurisdiction_id = jurisdiction.get("id")
|
|
167
|
+
iso2 = jurisdiction.get("iso2")
|
|
168
|
+
if not jurisdiction_id or not iso2 or not jurisdiction.get("name"):
|
|
169
|
+
errors.append(f"{prefix} requires id, iso2, and name")
|
|
170
|
+
continue
|
|
171
|
+
if jurisdiction_id in directory_ids:
|
|
172
|
+
errors.append(f"duplicate directory jurisdiction id: {jurisdiction_id}")
|
|
173
|
+
if iso2 in directory_iso2:
|
|
174
|
+
errors.append(f"duplicate directory ISO2: {iso2}")
|
|
175
|
+
directory_ids.add(jurisdiction_id)
|
|
176
|
+
directory_iso2.add(iso2)
|
|
177
|
+
|
|
178
|
+
for source_index, source in enumerate(directory.get("directory_sources", [])):
|
|
179
|
+
if not source.get("label") or not _valid_https_url(source.get("url", "")):
|
|
180
|
+
errors.append(f"directory.directory_sources[{source_index}] is invalid")
|
|
181
|
+
|
|
182
|
+
for jurisdiction in jurisdictions:
|
|
183
|
+
iso2 = jurisdiction.get("iso2")
|
|
184
|
+
if iso2 and iso2 not in directory_iso2:
|
|
185
|
+
errors.append(f"official source ISO2 not in directory: {iso2}")
|
|
186
|
+
|
|
187
|
+
return errors
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def print_jurisdiction(destination: dict, seed: dict | None) -> None:
|
|
191
|
+
support_level = seed["support_level"] if seed else "live_discovery"
|
|
192
|
+
print(f"{destination['name']} [{destination['id']}] - {support_level}")
|
|
193
|
+
if not seed:
|
|
194
|
+
print("No cached official source seed. Live official-source discovery is required.")
|
|
195
|
+
return
|
|
196
|
+
if seed.get("notes"):
|
|
197
|
+
print(f"Note: {seed['notes']}")
|
|
198
|
+
for source in seed["sources"]:
|
|
199
|
+
roles = ", ".join(source["roles"])
|
|
200
|
+
print(f"- {source['label']} ({roles})")
|
|
201
|
+
print(f" {source['url']}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def add_route_arguments(parser: argparse.ArgumentParser) -> None:
|
|
205
|
+
parser.add_argument("jurisdiction")
|
|
206
|
+
parser.add_argument("--nationality", default="Pending")
|
|
207
|
+
parser.add_argument("--passport-type", default="Ordinary passport")
|
|
208
|
+
parser.add_argument("--travel-document-issuer", default="Pending")
|
|
209
|
+
parser.add_argument("--residence", default="Pending")
|
|
210
|
+
parser.add_argument("--residence-status", default="Pending")
|
|
211
|
+
parser.add_argument("--application-location", default="Pending")
|
|
212
|
+
parser.add_argument("--purpose", default="Pending")
|
|
213
|
+
parser.add_argument("--arrival-date", default="Pending")
|
|
214
|
+
parser.add_argument("--duration", default="Pending")
|
|
215
|
+
parser.add_argument("--entries", default="Pending")
|
|
216
|
+
parser.add_argument("--arrival-mode", default="Pending")
|
|
217
|
+
parser.add_argument("--transit", default="None declared")
|
|
218
|
+
parser.add_argument("--existing-entry-rights", default="None declared")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def print_live_check_plan(destination: dict, seed: dict | None, args: argparse.Namespace) -> None:
|
|
222
|
+
print(f"Live visa-need check: {destination['name']}")
|
|
223
|
+
print(f"Nationality/status: {args.nationality}")
|
|
224
|
+
print(f"Travel document: {args.passport_type}; issuer: {args.travel_document_issuer}")
|
|
225
|
+
print(f"Legal residence/status: {args.residence}; {args.residence_status}")
|
|
226
|
+
print(f"Application location: {args.application_location}")
|
|
227
|
+
print(f"Purpose: {args.purpose}")
|
|
228
|
+
print(f"Arrival/duration/entries: {args.arrival_date}; {args.duration}; {args.entries}")
|
|
229
|
+
print(f"Arrival mode: {args.arrival_mode}")
|
|
230
|
+
print(f"Transit/self-transfer: {args.transit}")
|
|
231
|
+
print(f"Existing visas/residence permits: {args.existing_entry_rights}")
|
|
232
|
+
print("\nMandatory live research order:")
|
|
233
|
+
print("1. Open the destination immigration authority or foreign ministry.")
|
|
234
|
+
print("2. Find the current visa-exemption, ETA, eVisa, visa-on-arrival, and transit rules for this exact travel document.")
|
|
235
|
+
print("3. Check whether residence permits or third-country visas change eligibility.")
|
|
236
|
+
print("4. Resolve the responsible embassy/consulate and location-specific filing rules.")
|
|
237
|
+
print("5. Check Timatic/IATA for carrier boarding requirements; treat it as an operational cross-check, not legal authority.")
|
|
238
|
+
print("6. Record checked time, effective dates, conditions, conflicts, and a refresh deadline in the HTML.")
|
|
239
|
+
print("\nRequired result fields:")
|
|
240
|
+
print("- verdict: visa_free | eta | evisa | visa_on_arrival | consular_visa | transit_authorization | residence_route | mixed | unresolved")
|
|
241
|
+
print("- allowed purpose, maximum stay, entries, validity window, and extension rules")
|
|
242
|
+
print("- passport validity/blank pages, arrival ports/modes, onward travel, funds, insurance, accommodation, and registration conditions")
|
|
243
|
+
print("- transit and self-transfer result for every intermediate country")
|
|
244
|
+
print("- government source URLs, page dates/effective dates, checked timestamp, and conflict notes")
|
|
245
|
+
print("- Timatic/IATA or carrier cross-check result and checked timestamp")
|
|
246
|
+
print("\nOfficial starting sources:")
|
|
247
|
+
if seed:
|
|
248
|
+
for source in seed["sources"]:
|
|
249
|
+
print(f"- {source['label']}: {source['url']}")
|
|
250
|
+
else:
|
|
251
|
+
print(f"- Search for the official immigration authority or foreign ministry of {destination['name']}.")
|
|
252
|
+
print(f"- Search query: {destination['name']} official visa requirements {args.nationality} passport")
|
|
253
|
+
print(f"- Search query: {destination['name']} immigration eVisa ETA visa on arrival official")
|
|
254
|
+
print(f"- Search query: {destination['name']} embassy {args.application_location} visa official")
|
|
255
|
+
print(f"- IATA Travel Centre: {IATA_TRAVEL_CENTRE}")
|
|
256
|
+
print("\nGate: do not collect the full application form until the verdict is resolved or explicitly marked unresolved for applicant review.")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
BROWSER_USER_AGENT = (
|
|
260
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
261
|
+
"(KHTML, like Gecko) Chrome/125.0 Safari/537.36"
|
|
262
|
+
)
|
|
263
|
+
# Government hosts routinely bot-block automated clients with these codes or by
|
|
264
|
+
# dropping the connection. That means "guarded", not "dead" — we warn, not fail.
|
|
265
|
+
GUARDED_STATUSES = {401, 403, 405, 406, 429, 503}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def check_urls(registry: dict, timeout: float) -> tuple[list[str], list[str]]:
|
|
269
|
+
"""Probe every seeded source URL. Returns (dead, guarded).
|
|
270
|
+
|
|
271
|
+
`dead` = genuinely broken (404/410, DNS failure, refused connection) and
|
|
272
|
+
should fail CI. `guarded` = reachable but bot-blocked; reported as a warning
|
|
273
|
+
only, because government hosts commonly reject non-browser clients.
|
|
274
|
+
"""
|
|
275
|
+
import urllib.error
|
|
276
|
+
import urllib.request
|
|
277
|
+
|
|
278
|
+
dead: list[str] = []
|
|
279
|
+
guarded: list[str] = []
|
|
280
|
+
checked = 0
|
|
281
|
+
for jurisdiction in registry["jurisdictions"]:
|
|
282
|
+
for source in jurisdiction.get("sources", []):
|
|
283
|
+
url = source.get("url", "")
|
|
284
|
+
checked += 1
|
|
285
|
+
request = urllib.request.Request(
|
|
286
|
+
url,
|
|
287
|
+
headers={"User-Agent": BROWSER_USER_AGENT},
|
|
288
|
+
)
|
|
289
|
+
try:
|
|
290
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
291
|
+
status = response.status
|
|
292
|
+
except urllib.error.HTTPError as error:
|
|
293
|
+
if error.code in GUARDED_STATUSES:
|
|
294
|
+
guarded.append(f"{jurisdiction['id']} {url} -> HTTP {error.code}")
|
|
295
|
+
else:
|
|
296
|
+
dead.append(f"{jurisdiction['id']} {url} -> HTTP {error.code}")
|
|
297
|
+
continue
|
|
298
|
+
except urllib.error.URLError as error:
|
|
299
|
+
# A dropped connection from a live host is typically anti-bot;
|
|
300
|
+
# a DNS/name-resolution failure is a genuinely dead host.
|
|
301
|
+
reason = str(error.reason)
|
|
302
|
+
if "Name or service not known" in reason or "nodename" in reason:
|
|
303
|
+
dead.append(f"{jurisdiction['id']} {url} -> {error.reason}")
|
|
304
|
+
else:
|
|
305
|
+
guarded.append(f"{jurisdiction['id']} {url} -> {error.reason}")
|
|
306
|
+
continue
|
|
307
|
+
except Exception as error: # noqa: BLE001
|
|
308
|
+
guarded.append(f"{jurisdiction['id']} {url} -> {error}")
|
|
309
|
+
continue
|
|
310
|
+
if status in {404, 410}:
|
|
311
|
+
dead.append(f"{jurisdiction['id']} {url} -> HTTP {status}")
|
|
312
|
+
elif status >= 400:
|
|
313
|
+
guarded.append(f"{jurisdiction['id']} {url} -> HTTP {status}")
|
|
314
|
+
print(
|
|
315
|
+
f"Probed {checked} source URLs; {len(dead)} dead, {len(guarded)} guarded/unverified."
|
|
316
|
+
)
|
|
317
|
+
return dead, guarded
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def main() -> None:
|
|
321
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
322
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
323
|
+
subparsers.add_parser("validate")
|
|
324
|
+
check_urls_parser = subparsers.add_parser("check-urls")
|
|
325
|
+
check_urls_parser.add_argument("--timeout", type=float, default=20.0)
|
|
326
|
+
subparsers.add_parser("coverage")
|
|
327
|
+
list_parser = subparsers.add_parser("list")
|
|
328
|
+
list_parser.add_argument("--seeded-only", action="store_true")
|
|
329
|
+
show_parser = subparsers.add_parser("show")
|
|
330
|
+
show_parser.add_argument("jurisdiction")
|
|
331
|
+
add_route_arguments(subparsers.add_parser("live-check-plan"))
|
|
332
|
+
add_route_arguments(subparsers.add_parser("research-plan"))
|
|
333
|
+
args = parser.parse_args()
|
|
334
|
+
|
|
335
|
+
registry = load_registry()
|
|
336
|
+
directory = load_directory()
|
|
337
|
+
errors = validate(registry, directory)
|
|
338
|
+
if errors:
|
|
339
|
+
for error in errors:
|
|
340
|
+
print(f"ERROR: {error}")
|
|
341
|
+
raise SystemExit(1)
|
|
342
|
+
|
|
343
|
+
seeded_iso2 = {
|
|
344
|
+
jurisdiction["iso2"]
|
|
345
|
+
for jurisdiction in registry["jurisdictions"]
|
|
346
|
+
if jurisdiction.get("iso2")
|
|
347
|
+
}
|
|
348
|
+
if args.command == "validate":
|
|
349
|
+
print(
|
|
350
|
+
f"Valid registries: {len(directory['jurisdictions'])} destinations; "
|
|
351
|
+
f"{len(registry['jurisdictions'])} official source seeds"
|
|
352
|
+
)
|
|
353
|
+
elif args.command == "check-urls":
|
|
354
|
+
dead, guarded = check_urls(registry, args.timeout)
|
|
355
|
+
for warning in guarded:
|
|
356
|
+
print(f"GUARDED: {warning}")
|
|
357
|
+
for failure in dead:
|
|
358
|
+
print(f"DEAD: {failure}")
|
|
359
|
+
if dead:
|
|
360
|
+
raise SystemExit(1)
|
|
361
|
+
elif args.command == "coverage":
|
|
362
|
+
print(f"Searchable destinations: {len(directory['jurisdictions'])}")
|
|
363
|
+
print(f"Destinations with cached official seeds: {len(seeded_iso2)}")
|
|
364
|
+
print(f"Destinations requiring live source discovery: {len(directory['jurisdictions']) - len(seeded_iso2)}")
|
|
365
|
+
print(f"Additional route areas with seeds: {len(registry['jurisdictions']) - len(seeded_iso2)}")
|
|
366
|
+
elif args.command == "list":
|
|
367
|
+
for destination in directory["jurisdictions"]:
|
|
368
|
+
seed = find_jurisdiction(registry, destination["iso2"])
|
|
369
|
+
if args.seeded_only and not seed:
|
|
370
|
+
continue
|
|
371
|
+
support_level = seed["support_level"] if seed else "live_discovery"
|
|
372
|
+
print(f"{destination['id']}\t{destination['name']}\t{support_level}")
|
|
373
|
+
elif args.command == "show":
|
|
374
|
+
result = find_destination(registry, directory, args.jurisdiction)
|
|
375
|
+
if not result:
|
|
376
|
+
raise SystemExit(f"Unknown destination: {args.jurisdiction}")
|
|
377
|
+
print_jurisdiction(*result)
|
|
378
|
+
elif args.command in {"live-check-plan", "research-plan"}:
|
|
379
|
+
result = find_destination(registry, directory, args.jurisdiction)
|
|
380
|
+
if not result:
|
|
381
|
+
raise SystemExit(f"Unknown destination: {args.jurisdiction}")
|
|
382
|
+
print_live_check_plan(*result, args)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
if __name__ == "__main__":
|
|
386
|
+
main()
|