agent-bios 0.19.2 → 0.19.3
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/DEPENDENCIES.md +32 -4
- package/README.md +54 -4
- package/claude/guides/cli-multi-model-workflow.md +1 -1
- package/claude/guides/coding-staged-workflow.md +17 -0
- package/claude/guides/llm-capability-boundary.md +7 -1
- package/claude/guides/tooling-gotchas.md +20 -1
- package/claude/guides/ui-design/visual-direction.md +88 -0
- package/claude/guides/ui-design.md +90 -0
- package/claude/guides/verification-discipline.md +10 -1
- package/claude/hooks/tooling-gotchas-hook.py +41 -0
- package/codex/guides/cli-multi-model-workflow.md +1 -1
- package/codex/guides/coding-staged-workflow.md +17 -0
- package/codex/guides/llm-capability-boundary.md +7 -1
- package/codex/guides/tooling-gotchas.md +20 -1
- package/codex/guides/ui-design/visual-direction.md +88 -0
- package/codex/guides/ui-design.md +90 -0
- package/codex/guides/verification-discipline.md +10 -1
- package/compose/app_bridge/scripts/bridge.py +23 -6
- package/compose/app_desktop/server.py +250 -0
- package/compose/domains.json +1 -0
- package/compose/host_platform.py +121 -0
- package/compose/instructions-state.py +5 -2
- package/compose/instructions_app.py +281 -48
- package/compose/instructions_catalog.py +2 -2
- package/compose/instructions_import.py +19 -11
- package/compose/instructions_install.py +77 -11
- package/compose/instructions_session.py +6 -6
- package/compose/instructions_setup.py +41 -4
- package/compose/instructions_setup_cli.py +31 -10
- package/compose/instructions_setup_i18n.py +3 -0
- package/compose/instructions_store.py +6 -6
- package/compose/instructions_transaction.py +8 -6
- package/compose/instructions_ui_runtime.py +2 -1
- package/compose/native_cli.py +52 -0
- package/compose/runtime_entry.py +58 -0
- package/compose/windows_deploy.py +719 -0
- package/docs/instructions.md +1 -0
- package/docs/releases/0.19.3.md +107 -0
- package/docs/session-model.md +8 -0
- package/docs/setup.md +36 -0
- package/docs/windows.md +99 -0
- package/install.sh +1 -0
- package/launch/agent-launch.py +12 -4
- package/launch/agent-launch.zsh +11 -1
- package/package.json +8 -3
- package/provenance.json +1 -1
|
@@ -6,6 +6,10 @@ activation path: installed instructions are stored and verifiable, while a launc
|
|
|
6
6
|
activation creates the per-session snapshot and pin.
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
|
+
try:
|
|
10
|
+
from host_platform import WINDOWS, runtime_environment
|
|
11
|
+
except ImportError:
|
|
12
|
+
from .host_platform import WINDOWS, runtime_environment
|
|
9
13
|
|
|
10
14
|
import argparse
|
|
11
15
|
import base64
|
|
@@ -482,7 +486,7 @@ class InstructionsInstaller:
|
|
|
482
486
|
def _projection_matches(self, entry: dict[str, Any]) -> bool:
|
|
483
487
|
path, after = Path(entry["path"]), entry["after"]
|
|
484
488
|
return self._matches_version(path, after) and (
|
|
485
|
-
not after.get("exists") or path.stat().st_mode & 0o777 == int(entry["mode"]))
|
|
489
|
+
not after.get("exists") or WINDOWS or path.stat().st_mode & 0o777 == int(entry["mode"]))
|
|
486
490
|
|
|
487
491
|
def _old_record(self) -> dict[str, Any] | None:
|
|
488
492
|
return _read_json(self.record_path) if self.record_path.exists() else None
|
|
@@ -500,8 +504,8 @@ class InstructionsInstaller:
|
|
|
500
504
|
if current.is_symlink():
|
|
501
505
|
raise InstallError(f"owned destination has symlink ancestor: {current}")
|
|
502
506
|
|
|
503
|
-
def _expected_owned(self, release: Path) -> dict[str, str]:
|
|
504
|
-
expected = {str(self.bin_root / "agent-launch"): hashlib.sha256(self._launcher_body(release)).hexdigest(),
|
|
507
|
+
def _expected_owned(self, release: Path, launcher_runtime=None) -> dict[str, str]:
|
|
508
|
+
expected = {str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")): hashlib.sha256(self._launcher_body(release, launcher_runtime=launcher_runtime, current=False)).hexdigest(),
|
|
505
509
|
str(self.launch_root / "profiles.toml"): _sha256(release / "launch" / "agent-launch.toml")}
|
|
506
510
|
i18n = release / "launch" / "i18n"
|
|
507
511
|
for source in sorted(i18n.glob("*.toml")):
|
|
@@ -509,11 +513,11 @@ class InstructionsInstaller:
|
|
|
509
513
|
return expected
|
|
510
514
|
|
|
511
515
|
def _validate_owned_record(self, record: dict[str, Any], release: Path) -> None:
|
|
512
|
-
expected = self._expected_owned(release)
|
|
516
|
+
expected = self._expected_owned(release, record.get("launcher_runtime"))
|
|
513
517
|
launcher = record.get("launcher")
|
|
514
518
|
if launcher is not None:
|
|
515
|
-
if not isinstance(launcher, dict) or launcher.get("path") != str(self.bin_root / "agent-launch") \
|
|
516
|
-
or launcher.get("sha256") != expected[str(self.bin_root / "agent-launch")]:
|
|
519
|
+
if not isinstance(launcher, dict) or launcher.get("path") != str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")) \
|
|
520
|
+
or launcher.get("sha256") != expected[str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"))]:
|
|
517
521
|
raise InstallError("private install record has an invalid launcher ownership claim")
|
|
518
522
|
self._safe_owned_path(Path(launcher["path"]), self.bin_root)
|
|
519
523
|
seen: set[str] = set()
|
|
@@ -535,7 +539,7 @@ class InstructionsInstaller:
|
|
|
535
539
|
return None
|
|
536
540
|
|
|
537
541
|
def _write_owned(self, path: Path, content: bytes, old: dict[str, Any] | None, mode: int) -> dict[str, str] | None:
|
|
538
|
-
root = self.bin_root if path == self.bin_root / "agent-launch" else self.launch_root
|
|
542
|
+
root = self.bin_root if path == self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch") else self.launch_root
|
|
539
543
|
self._safe_owned_path(path, root)
|
|
540
544
|
expected = self._owned_hash(old, path)
|
|
541
545
|
if path.exists() and (path.is_symlink() or (expected is None and _sha256(path) != hashlib.sha256(content).hexdigest())
|
|
@@ -544,7 +548,35 @@ class InstructionsInstaller:
|
|
|
544
548
|
_atomic_bytes(path, content, mode)
|
|
545
549
|
return {"path": str(path), "sha256": hashlib.sha256(content).hexdigest()}
|
|
546
550
|
|
|
547
|
-
def
|
|
551
|
+
def _current_launcher_runtime(self):
|
|
552
|
+
if not WINDOWS:
|
|
553
|
+
return None
|
|
554
|
+
env = runtime_environment(self.env)
|
|
555
|
+
return {'schema_version': 1, 'python': env.get('AGENT_BIOS_PYTHON_EXECUTABLE', str(Path(sys.executable).resolve())),
|
|
556
|
+
'environment': env}
|
|
557
|
+
|
|
558
|
+
def _launcher_body(self, package_root: Path, *, launcher_runtime=None, current=True) -> bytes:
|
|
559
|
+
if WINDOWS:
|
|
560
|
+
spec = self._current_launcher_runtime() if current else launcher_runtime
|
|
561
|
+
if spec is None:
|
|
562
|
+
# Exact pre-binding Windows source format, for existing receipts.
|
|
563
|
+
return ('@echo off\r\n"' + sys.executable + '" "' + str(package_root / "compose/native_cli.py") + '" launch %*\r\n').encode("utf-8")
|
|
564
|
+
if (not isinstance(spec, dict) or set(spec) != {'schema_version', 'python', 'environment'}
|
|
565
|
+
or spec['schema_version'] != 1 or not isinstance(spec['python'], str)
|
|
566
|
+
or not Path(spec['python']).is_absolute() or not isinstance(spec['environment'], dict)):
|
|
567
|
+
raise InstallError('invalid recorded launcher runtime')
|
|
568
|
+
env = spec['environment']
|
|
569
|
+
allowed = {'AGENT_BIOS_PYTHON_ENTRY', 'AGENT_BIOS_PYTHON_DEPS', 'AGENT_BIOS_PYTHON_EXECUTABLE'}
|
|
570
|
+
if env and (set(env) != allowed or any(not isinstance(v, str) or not Path(v).is_absolute() for v in env.values())):
|
|
571
|
+
raise InstallError('invalid recorded launcher dependency binding')
|
|
572
|
+
argv = [spec['python']]
|
|
573
|
+
if env:
|
|
574
|
+
argv += ['-I', '-X', 'utf8', env['AGENT_BIOS_PYTHON_ENTRY'], '--dependencies', env['AGENT_BIOS_PYTHON_DEPS'], '--script']
|
|
575
|
+
argv += [str(package_root / 'compose/native_cli.py'), 'launch']
|
|
576
|
+
if any(any(c in value for c in '\r\n\0"') for value in argv):
|
|
577
|
+
raise InstallError('unsafe launcher runtime argument')
|
|
578
|
+
command = ' '.join('"' + value.replace('%', '%%') + '"' for value in argv)
|
|
579
|
+
return ('@echo off\r\nsetlocal DisableDelayedExpansion\r\nchcp 65001 >nul\r\n' + command + ' %*\r\n').encode('utf-8')
|
|
548
580
|
quoted = shlex.quote(str(package_root))
|
|
549
581
|
# Verify historical ownership using the exact released launcher vocabulary.
|
|
550
582
|
canonical = (package_root / "compose/instructions_store.py").is_file()
|
|
@@ -564,6 +596,36 @@ class InstructionsInstaller:
|
|
|
564
596
|
spec.loader.exec_module(module)
|
|
565
597
|
return module.ShellIntegration(self.env, self.repo)
|
|
566
598
|
|
|
599
|
+
def launcher_reachability(self) -> dict[str, Any]:
|
|
600
|
+
"""Report whether the launcher this install deposits can actually be typed.
|
|
601
|
+
|
|
602
|
+
Both answers are known here and invisible in the installed tree: install
|
|
603
|
+
writes the launcher into a directory the caller's PATH may not carry, and
|
|
604
|
+
the shell connection that reaches it for bare host commands is opt-in. A
|
|
605
|
+
completion summary that omits them reports a success the caller cannot use.
|
|
606
|
+
Read-only, and never raises: a summary is not worth failing an install
|
|
607
|
+
that succeeded, and reporting must not become a reason to write.
|
|
608
|
+
"""
|
|
609
|
+
launcher = self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")
|
|
610
|
+
connected: bool | None = None
|
|
611
|
+
if not WINDOWS:
|
|
612
|
+
# zsh is the only shell this connection speaks; elsewhere the question
|
|
613
|
+
# has no answer rather than a negative one.
|
|
614
|
+
try:
|
|
615
|
+
connected = bool(self._shell_manager().status().get("enabled"))
|
|
616
|
+
except (InstallError, OSError, RuntimeError, ValueError):
|
|
617
|
+
connected = False
|
|
618
|
+
try:
|
|
619
|
+
target = os.path.normcase(os.path.realpath(self.bin_root))
|
|
620
|
+
# No expanduser: a shell does not tilde-expand PATH entries when it
|
|
621
|
+
# searches, so a literal ~ there really does not find the command.
|
|
622
|
+
on_path = any(os.path.normcase(os.path.realpath(part)) == target
|
|
623
|
+
for part in self.env.get("PATH", "").split(os.pathsep) if part)
|
|
624
|
+
except (OSError, ValueError):
|
|
625
|
+
on_path = True # Undecidable; do not tell the caller PATH is wrong on a guess.
|
|
626
|
+
return {"path": str(launcher), "directory": str(self.bin_root),
|
|
627
|
+
"on_path": on_path, "shell_connected": connected}
|
|
628
|
+
|
|
567
629
|
def setup_catalog(self) -> dict[str, Any]:
|
|
568
630
|
return self._catalog(self.repo)
|
|
569
631
|
|
|
@@ -650,6 +712,8 @@ class InstructionsInstaller:
|
|
|
650
712
|
return result
|
|
651
713
|
|
|
652
714
|
def _shell_paths(self, action: str) -> list[dict[str, Any]]:
|
|
715
|
+
if WINDOWS:
|
|
716
|
+
return []
|
|
653
717
|
try:
|
|
654
718
|
manager = self._shell_manager()
|
|
655
719
|
# A receipt-less managed script may survive an interrupted opt-in.
|
|
@@ -736,7 +800,7 @@ class InstructionsInstaller:
|
|
|
736
800
|
if not isinstance(baseline_ref, str):
|
|
737
801
|
raise InstallError("private instructions store candidate has no baseline ref")
|
|
738
802
|
owned: list[tuple[Path, bytes, int]] = [
|
|
739
|
-
(self.bin_root / "agent-launch", self._launcher_body(release), 0o755),
|
|
803
|
+
(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"), self._launcher_body(release), 0o755),
|
|
740
804
|
(self.launch_root / "profiles.toml", (release / "launch" / "agent-launch.toml").read_bytes(), 0o644),
|
|
741
805
|
]
|
|
742
806
|
owned.extend((self.launch_root / "i18n" / source.name, source.read_bytes(), 0o644)
|
|
@@ -759,6 +823,8 @@ class InstructionsInstaller:
|
|
|
759
823
|
"launcher": launcher, "config_files": config, "created_at": int(time.time()),
|
|
760
824
|
"mode": "private-session-scoped", "needs_action": [],
|
|
761
825
|
}
|
|
826
|
+
if WINDOWS:
|
|
827
|
+
record["launcher_runtime"] = self._current_launcher_runtime()
|
|
762
828
|
if selection_mode is not None:
|
|
763
829
|
record["selection_mode"] = selection_mode
|
|
764
830
|
paths = [{"path": str(path), "before": self._file_version(path),
|
|
@@ -990,7 +1056,7 @@ class InstructionsInstaller:
|
|
|
990
1056
|
local = [self.launch_root / name for name in ("presets.local.toml", "review-methods.local.toml", "launcher.local.toml")]
|
|
991
1057
|
connections = self.launch_root.parent / "agent-bios"
|
|
992
1058
|
cleanup = [*local, connections / "ingest-url", self.user_root / "understand" / "state.json"]
|
|
993
|
-
owned = [(self.bin_root / "agent-launch", self._launcher_body(release), 0o755),
|
|
1059
|
+
owned = [(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"), self._launcher_body(release, launcher_runtime=record.get("launcher_runtime"), current=False), 0o755),
|
|
994
1060
|
(self.launch_root / "profiles.toml", (release / "launch" / "agent-launch.toml").read_bytes(), 0o644)]
|
|
995
1061
|
owned.extend((self.launch_root / "i18n" / p.name, p.read_bytes(), 0o644)
|
|
996
1062
|
for p in sorted((release / "launch" / "i18n").glob("*.toml")))
|
|
@@ -1190,7 +1256,7 @@ class InstructionsInstaller:
|
|
|
1190
1256
|
("wrappers/codex-run.sh", self.codex_root / "bin" / "codex-run"),
|
|
1191
1257
|
("wrappers/codex-helm.sh", self.codex_root / "bin" / "codex-helm"),
|
|
1192
1258
|
("wrappers/claude-run.sh", self.claude_root / "bin" / "claude-run"),
|
|
1193
|
-
("launch/agent-launch.py", self.bin_root / "agent-launch"),
|
|
1259
|
+
("launch/agent-launch.py", self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")),
|
|
1194
1260
|
("launch/agent-launch.toml", self.launch_root / "profiles.toml"),
|
|
1195
1261
|
("launch/agent-launch.zsh", self.launch_root / "shell.zsh"),
|
|
1196
1262
|
)
|
|
@@ -4,6 +4,10 @@ Native homes stay native. The only files this module writes are private activati
|
|
|
4
4
|
records and pins; no global instructions, auth, or discovery registration is copied.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
|
+
try:
|
|
8
|
+
from host_platform import sync_directory, cli_argv
|
|
9
|
+
except ImportError:
|
|
10
|
+
from .host_platform import sync_directory, cli_argv
|
|
7
11
|
|
|
8
12
|
import hashlib
|
|
9
13
|
import json
|
|
@@ -144,18 +148,14 @@ def atomic_json(path, data):
|
|
|
144
148
|
raise SessionError(f"refusing symlink: {path}")
|
|
145
149
|
tmp = path.with_name(path.name + '.' + uuid.uuid4().hex + '.tmp')
|
|
146
150
|
try:
|
|
147
|
-
with open(tmp, 'x', encoding='utf-8') as out:
|
|
151
|
+
with open(tmp, 'x', encoding='utf-8', newline='\n') as out:
|
|
148
152
|
os.chmod(tmp, 0o600)
|
|
149
153
|
json.dump(data, out, ensure_ascii=False, indent=2)
|
|
150
154
|
out.write('\n')
|
|
151
155
|
out.flush()
|
|
152
156
|
os.fsync(out.fileno())
|
|
153
157
|
os.replace(tmp, path)
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
os.fsync(fd)
|
|
157
|
-
finally:
|
|
158
|
-
os.close(fd)
|
|
158
|
+
sync_directory(path.parent)
|
|
159
159
|
finally:
|
|
160
160
|
tmp.unlink(missing_ok=True)
|
|
161
161
|
|
|
@@ -6,6 +6,11 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import platform
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from host_platform import runtime_environment
|
|
12
|
+
except ImportError:
|
|
13
|
+
from .host_platform import runtime_environment
|
|
9
14
|
import re
|
|
10
15
|
import shlex
|
|
11
16
|
import shutil
|
|
@@ -44,7 +49,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
|
|
|
44
49
|
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
45
50
|
home = Path(env.get("HOME", str(Path.home())))
|
|
46
51
|
system = system or platform.system()
|
|
47
|
-
supported = system in {"Darwin", "Linux"}
|
|
52
|
+
supported = system in {"Darwin", "Linux", "Windows"}
|
|
48
53
|
paths = {name: which(name, path=env.get("PATH", os.defpath))
|
|
49
54
|
for name in ("bash", "python3", "node", "npm", "brew", "git", "zsh", "claude", "codex", "cp", "mktemp")}
|
|
50
55
|
|
|
@@ -76,7 +81,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
|
|
|
76
81
|
brew_row = add("homebrew", "Homebrew", "optional package manager", "Offers local package installation recipes when available.",
|
|
77
82
|
path=paths["brew"], argv=[paths["brew"], "--version"] if paths["brew"] else None,
|
|
78
83
|
reason="Optional: use an existing operating-system package manager or the official host installer.")
|
|
79
|
-
brew = paths["brew"] if brew_row["status"] == "available" else None
|
|
84
|
+
brew = paths["brew"] if brew_row["status"] == "available" and system != "Windows" else None
|
|
80
85
|
|
|
81
86
|
def formula(name: str) -> list[str] | None:
|
|
82
87
|
return [brew, "install", name] if brew else None
|
|
@@ -84,7 +89,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
|
|
|
84
89
|
add("bash", "Bash", "runtime", "Runs the package entry point and bundled shell tools.",
|
|
85
90
|
path=paths["bash"], argv=[paths["bash"], "--version"] if paths["bash"] else None,
|
|
86
91
|
action=formula("bash"), scope="Homebrew prefix", reason="Install Bash using your operating system package manager.")
|
|
87
|
-
py = paths["python3"]
|
|
92
|
+
py = sys.executable if system == "Windows" else paths["python3"]
|
|
88
93
|
add("python3", "Python 3.11+", "runtime", "Runs installation, instructions storage, and terminal interfaces.", path=py,
|
|
89
94
|
argv=[py, "-c", "import sys; print(sys.version.split()[0]); raise SystemExit(sys.version_info < (3,11))"] if py else None,
|
|
90
95
|
action=formula("python"), scope="Homebrew prefix", reason="Install Python 3.11+ and ensure python3 resolves to it.")
|
|
@@ -139,12 +144,16 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
|
|
|
139
144
|
code += "; from jsonschema import Draft202012Validator"
|
|
140
145
|
if managed and name in pins:
|
|
141
146
|
code += f"; import sys; raise SystemExit(sys.version_info < (3, 11) or actual != {pins[name]!r})"
|
|
147
|
+
binding = runtime_environment(env)
|
|
148
|
+
if binding and name == "jsonschema" and not managed:
|
|
149
|
+
return [interpreter, "-I", "-X", "utf8", binding['AGENT_BIOS_PYTHON_ENTRY'],
|
|
150
|
+
'--dependencies', binding['AGENT_BIOS_PYTHON_DEPS'], '--probe', name]
|
|
142
151
|
return [interpreter, "-c", code]
|
|
143
152
|
|
|
144
153
|
bootstrap = add("python-venv", "Python venv / pip bootstrap", "managed dependency prerequisite", "Creates the managed environment using AGENT_LAUNCH_PYTHON when set, otherwise python3.", path=bootstrap_python,
|
|
145
154
|
argv=[bootstrap_python, "-c", "import sys, venv, ensurepip; print('venv; bundled pip ' + ensurepip.version()); raise SystemExit(sys.version_info < (3, 11))"] if bootstrap_python else None,
|
|
146
155
|
reason="Install venv/ensurepip for Python 3.11+; check AGENT_LAUNCH_PYTHON if configured. An existing managed environment does not need this bootstrap.")
|
|
147
|
-
can_provision = bool(paths["bash"] and provisioner.is_file() and (vpy.is_file() or bootstrap["status"] == "available"))
|
|
156
|
+
can_provision = system != "Windows" and bool(paths["bash"] and provisioner.is_file() and (vpy.is_file() or bootstrap["status"] == "available"))
|
|
148
157
|
try:
|
|
149
158
|
from .instructions_ui_runtime import runtime_inventory
|
|
150
159
|
except ImportError:
|
|
@@ -181,6 +190,8 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
|
|
|
181
190
|
row = add(identifier, title, "optional job / personal integration", purpose, present=False,
|
|
182
191
|
reason="Configure this only for a workflow that requires it; setup cannot choose your job environment or account.")
|
|
183
192
|
row["status"] = "not assessed"
|
|
193
|
+
if system == "Windows":
|
|
194
|
+
rows = [row for row in rows if row["id"] not in {"homebrew", "bash", "zsh", "cp", "mktemp", "python-bootstrap", "python-venv"}]
|
|
184
195
|
return rows
|
|
185
196
|
|
|
186
197
|
|
|
@@ -226,6 +237,25 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
|
|
|
226
237
|
def recovery_hint() -> str:
|
|
227
238
|
return t("Use the returned review_id with agent-bios setup status or agent-bios setup resume before continuing.")
|
|
228
239
|
|
|
240
|
+
def reachability_lines() -> list[str]:
|
|
241
|
+
"""Name the two ways a finished install still cannot be reached.
|
|
242
|
+
|
|
243
|
+
install() leaves a launcher in a directory the caller's PATH may not
|
|
244
|
+
carry, and the shell connection that reaches it for bare host commands is
|
|
245
|
+
opt-in. Neither is visible in the installed tree, so a summary that omits
|
|
246
|
+
them reports a success the caller cannot use. Absent facts say nothing.
|
|
247
|
+
"""
|
|
248
|
+
reach = result.get("launcher") or {}
|
|
249
|
+
lines: list[str] = []
|
|
250
|
+
if reach.get("on_path") is False:
|
|
251
|
+
lines.append(t("The launcher is installed at {path}, but {directory} is not on this shell's PATH.",
|
|
252
|
+
path=str(reach.get("path")), directory=str(reach.get("directory"))))
|
|
253
|
+
lines.append(t("Add {directory} to the PATH in your shell startup file, or run the launcher by its full path.",
|
|
254
|
+
directory=str(reach.get("directory"))))
|
|
255
|
+
if reach.get("shell_connected") is False:
|
|
256
|
+
lines.append(t("Typing claude or codex alone does not open the launcher: the optional shell connection is off. Turn it on with agent-bios shell restore."))
|
|
257
|
+
return lines
|
|
258
|
+
|
|
229
259
|
if result.get("cancelled"):
|
|
230
260
|
if result.get("cancelled_after_start"):
|
|
231
261
|
lines = [t("Setup stopped after the current operation finished.")]
|
|
@@ -262,6 +292,7 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
|
|
|
262
292
|
lines.append(t("The app command registration is retained; instruction capture did not complete."))
|
|
263
293
|
if completed:
|
|
264
294
|
lines.append(t("Dependencies installed: {dependencies}.", dependencies=", ".join(completed)))
|
|
295
|
+
lines.extend(reachability_lines())
|
|
265
296
|
lines.append(recovery_hint() if interface == "conversation" else t("Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel)."))
|
|
266
297
|
return "\n".join(lines)
|
|
267
298
|
if not result.get("applied"):
|
|
@@ -301,6 +332,7 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
|
|
|
301
332
|
if request == f"Use $agent-bios to import capture {captured['capture_id']}":
|
|
302
333
|
request = t("Use $agent-bios to import capture {capture_id}", capture_id=captured["capture_id"])
|
|
303
334
|
lines.append(t("In Codex: {request}", request=request))
|
|
335
|
+
lines.extend(reachability_lines())
|
|
304
336
|
lines.append(t("Continue in the conversation using the returned verified entrypoint.") if interface == "conversation"
|
|
305
337
|
else t("Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel)."))
|
|
306
338
|
return "\n".join(lines)
|
|
@@ -516,6 +548,11 @@ class SetupController:
|
|
|
516
548
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
517
549
|
result["installation_error"] = str(exc)
|
|
518
550
|
return result
|
|
551
|
+
# Read once the installation is durable, so the extras failure path reports
|
|
552
|
+
# the same reachability as the clean one: both leave a launcher behind.
|
|
553
|
+
reachability = getattr(self.installer, "launcher_reachability", None)
|
|
554
|
+
if callable(reachability):
|
|
555
|
+
result["launcher"] = reachability()
|
|
519
556
|
if cancelled():
|
|
520
557
|
return result
|
|
521
558
|
try:
|
|
@@ -20,6 +20,11 @@ from typing import Any, Callable
|
|
|
20
20
|
if __name__ == "__main__":
|
|
21
21
|
sys.dont_write_bytecode = True
|
|
22
22
|
|
|
23
|
+
try:
|
|
24
|
+
from host_platform import sync_directory, cli_argv, python_argv, runtime_environment
|
|
25
|
+
except ImportError:
|
|
26
|
+
from .host_platform import sync_directory, cli_argv, python_argv, runtime_environment
|
|
27
|
+
|
|
23
28
|
try:
|
|
24
29
|
from instructions_install import InstructionsInstaller
|
|
25
30
|
from instructions_setup import SetupController, SetupError, format_setup_result, review_summary
|
|
@@ -91,6 +96,24 @@ def _identity() -> dict[str, Any]:
|
|
|
91
96
|
|
|
92
97
|
|
|
93
98
|
def _process_identity(pid: int) -> str | None:
|
|
99
|
+
if os.name == "nt":
|
|
100
|
+
import ctypes
|
|
101
|
+
from ctypes import wintypes
|
|
102
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
103
|
+
kernel.OpenProcess.restype = wintypes.HANDLE
|
|
104
|
+
kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
105
|
+
kernel.GetProcessTimes.argtypes = [wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4
|
|
106
|
+
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
107
|
+
handle = kernel.OpenProcess(0x1000, False, pid)
|
|
108
|
+
if not handle:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
times = [wintypes.FILETIME() for _ in range(4)]
|
|
112
|
+
if not kernel.GetProcessTimes(handle, *(ctypes.byref(t) for t in times)):
|
|
113
|
+
return None
|
|
114
|
+
return str((times[0].dwHighDateTime << 32) | times[0].dwLowDateTime)
|
|
115
|
+
finally:
|
|
116
|
+
kernel.CloseHandle(handle)
|
|
94
117
|
if sys.platform.startswith("linux"):
|
|
95
118
|
try:
|
|
96
119
|
fields = (Path("/proc") / str(pid) / "stat").read_text().rsplit(")", 1)[1].split()
|
|
@@ -131,11 +154,7 @@ def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
|
131
154
|
os.fsync(output.fileno())
|
|
132
155
|
temporary.chmod(0o600)
|
|
133
156
|
os.replace(temporary, path)
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
os.fsync(directory)
|
|
137
|
-
finally:
|
|
138
|
-
os.close(directory)
|
|
157
|
+
sync_directory(path.parent)
|
|
139
158
|
finally:
|
|
140
159
|
temporary.unlink(missing_ok=True)
|
|
141
160
|
|
|
@@ -191,7 +210,7 @@ class SetupService:
|
|
|
191
210
|
|
|
192
211
|
def start(self, language: str | None = None) -> dict[str, Any]:
|
|
193
212
|
package = Path(self.installer.repo).resolve()
|
|
194
|
-
cli =
|
|
213
|
+
cli = cli_argv(package)
|
|
195
214
|
return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-start",
|
|
196
215
|
"languages": [{"label": label, "id": identifier} for label, identifier in LANGUAGES],
|
|
197
216
|
"suggested_language": detect_language(self.installer.env), "language": self._language(language),
|
|
@@ -302,6 +321,7 @@ class SetupService:
|
|
|
302
321
|
"AGENT_BIOS_INSTRUCTIONS_DIR": context["user_dir"], "CODEX_HOME": context["codex_dir"],
|
|
303
322
|
"AGENT_BIOS_CORPUS_DIR": context["user_dir"],
|
|
304
323
|
"CLAUDE_CONFIG_DIR": context["claude_dir"]}, "needs_action": []}
|
|
324
|
+
result["environment"].update(runtime_environment())
|
|
305
325
|
try:
|
|
306
326
|
with try_transaction_lock(Path(context["state_dir"])) as acquired:
|
|
307
327
|
if not acquired:
|
|
@@ -312,7 +332,8 @@ class SetupService:
|
|
|
312
332
|
return self._verified_handoff(context, result)
|
|
313
333
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
314
334
|
result["needs_action"].append(str(exc))
|
|
315
|
-
|
|
335
|
+
result["environment"].update(runtime_environment())
|
|
336
|
+
return result
|
|
316
337
|
|
|
317
338
|
def _verified_handoff(self, context: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
|
|
318
339
|
record = Path(context["state_dir"]) / "runtime/private-install.json"
|
|
@@ -322,8 +343,8 @@ class SetupService:
|
|
|
322
343
|
reject_symlink_ancestors(record)
|
|
323
344
|
release = confirmed_release(Path(context["state_dir"]))
|
|
324
345
|
result.update(package_verified=True, package_root=str(release), release_digest=release.name,
|
|
325
|
-
cli_argv=
|
|
326
|
-
setup_argv=
|
|
346
|
+
cli_argv=cli_argv(release),
|
|
347
|
+
setup_argv=cli_argv(release, "setup"), guide_path=str(release / "compose/setup/START.md"))
|
|
327
348
|
result["environment"].update(AGENT_BIOS_PACKAGE_ROOT=str(release), AGENT_BIOS_PRIVATE_INSTRUCTIONS="1", AGENT_BIOS_PRIVATE_CORPUS="1", AGENT_BIOS_LEGACY_INSTALL="0")
|
|
328
349
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
329
350
|
result["needs_action"].append(str(exc))
|
|
@@ -348,7 +369,7 @@ class SetupService:
|
|
|
348
369
|
result["needs_action"].extend(bridge.get("needs_action", []))
|
|
349
370
|
if bridge.get("registered") and not bridge.get("needs_action"):
|
|
350
371
|
helper = manager.target / "scripts/bridge.py"
|
|
351
|
-
result.update(helper_verified=True, helper_argv=
|
|
372
|
+
result.update(helper_verified=True, helper_argv=python_argv(helper))
|
|
352
373
|
if "AGENT_LAUNCH_VENV" in self.installer.env:
|
|
353
374
|
result["environment"]["AGENT_LAUNCH_VENV"] = self.installer.env["AGENT_LAUNCH_VENV"]
|
|
354
375
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
@@ -172,6 +172,9 @@ MESSAGES: dict[str, tuple[str, str]] = {
|
|
|
172
172
|
"Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel).": ("Instructions Studio 열기: 터미널 또는 Codex 앱 터미널 패널에서 agent-bios instructions를 실행하세요.", "Instructions Studioを開くには、ターミナルまたはCodexアプリのターミナルパネルでagent-bios instructionsを実行してください。"),
|
|
173
173
|
"Setup did not complete.": ("설정을 완료하지 못했습니다.", "セットアップを完了できませんでした。"),
|
|
174
174
|
"Setup complete. Private runtime installed.": ("설정을 완료하고 개인 실행 환경을 설치했습니다.", "セットアップが完了し、専用の実行環境をインストールしました。"),
|
|
175
|
+
"The launcher is installed at {path}, but {directory} is not on this shell's PATH.": ("실행기를 {path}에 설치했지만, 현재 셸의 PATH에 {directory} 경로가 없습니다.", "ランチャーを{path}にインストールしましたが、{directory}は現在のシェルのPATHにありません。"),
|
|
176
|
+
"Add {directory} to the PATH in your shell startup file, or run the launcher by its full path.": ("셸 시작 파일의 PATH에 {directory} 경로를 추가하거나, 실행기를 전체 경로로 실행하세요.", "シェルの起動ファイルのPATHに{directory}を追加するか、ランチャーをフルパスで実行してください。"),
|
|
177
|
+
"Typing claude or codex alone does not open the launcher: the optional shell connection is off. Turn it on with agent-bios shell restore.": ("claude 또는 codex만 입력해도 실행기가 열리지 않습니다. 선택적 셸 연결이 꺼져 있습니다. agent-bios shell restore로 켜세요.", "claudeまたはcodexだけを入力してもランチャーは開きません。任意のシェル連携が無効です。agent-bios shell restoreで有効にしてください。"),
|
|
175
178
|
"Instructions for future activated sessions: none.": ("앞으로 활성화하는 세션에서는 지침을 사용하지 않습니다.", "今後有効にするセッションでは指示を使用しません。"),
|
|
176
179
|
"Instructions for future activated sessions: {instructions}.": ("앞으로 활성화하는 세션에서 사용할 지침: {instructions}.", "今後有効にするセッションで使用する指示:{instructions}。"),
|
|
177
180
|
"Instructions policy: core, infrastructure and personal instructions.": ("지침 정책: 핵심·기반·개인 지침을 사용합니다.", "指示ポリシー:コア・基盤・個人指示を使用します。"),
|
|
@@ -6,6 +6,10 @@ does not alter a host configuration or claim that a snapshot was loaded by a
|
|
|
6
6
|
host; that is the launch adapter's job.
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
|
+
try:
|
|
10
|
+
from host_platform import sync_directory, cli_argv
|
|
11
|
+
except ImportError:
|
|
12
|
+
from .host_platform import sync_directory, cli_argv
|
|
9
13
|
|
|
10
14
|
import copy
|
|
11
15
|
import contextlib
|
|
@@ -126,11 +130,7 @@ def _atomic_write(path: Path, value: Any) -> None:
|
|
|
126
130
|
tmp.chmod(path.stat().st_mode & 0o777)
|
|
127
131
|
os.replace(tmp, path)
|
|
128
132
|
# Persist the directory entry as well as the file bytes.
|
|
129
|
-
|
|
130
|
-
try:
|
|
131
|
-
os.fsync(directory)
|
|
132
|
-
finally:
|
|
133
|
-
os.close(directory)
|
|
133
|
+
sync_directory(path.parent)
|
|
134
134
|
finally:
|
|
135
135
|
with contextlib.suppress(FileNotFoundError):
|
|
136
136
|
tmp.unlink()
|
|
@@ -151,7 +151,7 @@ def _rewrite_staged_paths(staging: Path, destination: Path) -> None:
|
|
|
151
151
|
except UnicodeDecodeError as exc:
|
|
152
152
|
raise InstructionsStoreError(f"snapshot compiler emitted non-text member: {path}") from exc
|
|
153
153
|
if old in text:
|
|
154
|
-
path.write_text(text.replace(old, new), encoding="utf-8")
|
|
154
|
+
path.write_text(text.replace(old, new), encoding="utf-8", newline="\n")
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def _snapshot_relative_paths(files: Any) -> list[str]:
|
|
@@ -9,7 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import contextlib
|
|
11
11
|
import base64
|
|
12
|
-
|
|
12
|
+
try:
|
|
13
|
+
from host_platform import file_locks as fcntl, redirected
|
|
14
|
+
except ImportError:
|
|
15
|
+
from .host_platform import file_locks as fcntl, redirected
|
|
13
16
|
import hashlib
|
|
14
17
|
import json
|
|
15
18
|
import os
|
|
@@ -57,7 +60,7 @@ def reject_symlink_ancestors(path: Path) -> None:
|
|
|
57
60
|
a same-named link elsewhere, or a user-owned link.
|
|
58
61
|
"""
|
|
59
62
|
for ancestor in (path, *path.parents):
|
|
60
|
-
if not ancestor
|
|
63
|
+
if not redirected(ancestor):
|
|
61
64
|
continue
|
|
62
65
|
system_alias = (sys.platform == "darwin" and ancestor != path
|
|
63
66
|
and ancestor in {Path("/var"), Path("/tmp"), Path("/etc")}
|
|
@@ -82,10 +85,9 @@ def _depths(name: str) -> dict[str, int]:
|
|
|
82
85
|
@contextlib.contextmanager
|
|
83
86
|
def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
|
|
84
87
|
root = Path(state_root).expanduser()
|
|
85
|
-
if root
|
|
88
|
+
if redirected(root):
|
|
86
89
|
raise TransactionError(f"unsafe instructions state root: {root}")
|
|
87
|
-
|
|
88
|
-
reject_symlink_ancestors(root)
|
|
90
|
+
reject_symlink_ancestors(root)
|
|
89
91
|
key = _key(root)
|
|
90
92
|
depths = _depths("lock_depths")
|
|
91
93
|
if depths.get(key, 0):
|
|
@@ -101,7 +103,7 @@ def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
|
|
|
101
103
|
lock = root / ".corpus-store.lock"
|
|
102
104
|
if lock.is_symlink():
|
|
103
105
|
raise TransactionError(f"unsafe instructions transaction lock: {lock}")
|
|
104
|
-
flags = (os.O_RDONLY | os
|
|
106
|
+
flags = ((os.O_RDWR if os.name == "nt" else os.O_RDONLY) | getattr(os, "O_NONBLOCK", 0) if readonly else os.O_CREAT | os.O_RDWR) | getattr(os, "O_NOFOLLOW", 0)
|
|
105
107
|
try:
|
|
106
108
|
descriptor = os.open(lock, flags, 0o600)
|
|
107
109
|
except FileNotFoundError:
|
|
@@ -7,6 +7,7 @@ import hashlib
|
|
|
7
7
|
import importlib
|
|
8
8
|
import io
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
from pathlib import Path, PurePosixPath
|
|
11
12
|
import re
|
|
12
13
|
import stat
|
|
@@ -255,7 +256,7 @@ def activate_ui_runtime(repo: Path) -> Path:
|
|
|
255
256
|
return active
|
|
256
257
|
parent = Path(tempfile.gettempdir()).resolve()
|
|
257
258
|
home = Path.home().resolve()
|
|
258
|
-
if parent == home or home in parent.parents:
|
|
259
|
+
if os.name != "nt" and (parent == home or home in parent.parents):
|
|
259
260
|
parent = Path("/tmp").resolve()
|
|
260
261
|
temporary = tempfile.TemporaryDirectory(prefix="agent-bios-ui-", dir=parent)
|
|
261
262
|
target = Path(temporary.name)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Bash-free entrypoint for the installed Windows application and direct Python use."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import runpy
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
sys.path.insert(0, str(ROOT / 'compose'))
|
|
11
|
+
sys.dont_write_bytecode = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(args=None):
|
|
15
|
+
args = list(sys.argv[1:] if args is None else args)
|
|
16
|
+
os.environ.setdefault('HOME', str(Path.home()))
|
|
17
|
+
os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
|
|
18
|
+
if args and args[0] in ('--version', '-V'):
|
|
19
|
+
print(json.loads((ROOT / 'package.json').read_text(encoding='utf-8'))['version'])
|
|
20
|
+
return 0
|
|
21
|
+
command = args.pop(0) if args else ('instructions' if (Path(os.environ.get('AGENT_BIOS_STATE_DIR', str(Path.home()/'.local/share/agent-bios'))) / 'runtime/private-install.json').exists() else 'install')
|
|
22
|
+
if command in ('help', '--help', '-h'):
|
|
23
|
+
print('agent-bios: install | instructions | setup | app | import | learn | understand | launch | verify | status | uninstall | reset')
|
|
24
|
+
return 0
|
|
25
|
+
modules = {'instructions':'instructions', 'corpus':'instructions', 'setup':'instructions_setup_cli',
|
|
26
|
+
'app':'instructions_app', 'import':'instructions_import', 'understand':'instructions_understand'}
|
|
27
|
+
if command in ('install', 'onboard', 'verify', 'status', 'uninstall', 'reset'):
|
|
28
|
+
import instructions_install
|
|
29
|
+
return instructions_install.main(['--repo', str(ROOT), command, *args])
|
|
30
|
+
if command in modules:
|
|
31
|
+
import importlib
|
|
32
|
+
module = importlib.import_module(modules[command])
|
|
33
|
+
if command in ('instructions', 'corpus'):
|
|
34
|
+
return module.main(['--repo', str(ROOT), *args], bundled_ui=True)
|
|
35
|
+
return module.main(['--repo', str(ROOT), *args])
|
|
36
|
+
if command == 'launch':
|
|
37
|
+
os.environ['AGENT_BIOS_PACKAGE_ROOT'] = str(ROOT)
|
|
38
|
+
os.environ['AGENT_BIOS_PRIVATE_INSTRUCTIONS'] = '1'
|
|
39
|
+
os.environ.pop('AGENT_BIOS_PRIVATE_CORPUS', None)
|
|
40
|
+
sys.argv = [str(ROOT / 'launch/agent-launch.py'), *args]
|
|
41
|
+
runpy.run_path(sys.argv[0], run_name='__main__')
|
|
42
|
+
return 0
|
|
43
|
+
if command == 'learn':
|
|
44
|
+
sys.argv = [str(ROOT / 'learn/collect-learning.py'), *args]
|
|
45
|
+
runpy.run_path(sys.argv[0], run_name='__main__')
|
|
46
|
+
return 0
|
|
47
|
+
print(f'Unknown or unsupported native command: {command}', file=sys.stderr)
|
|
48
|
+
return 2
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == '__main__':
|
|
52
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Application dependency entry for ordinary and isolated embeddable Python."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import runpy
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv=None):
|
|
13
|
+
# A Windows PowerShell 5.1 caller may prefix piped input with a UTF-8 BOM.
|
|
14
|
+
for stream, encoding in ((sys.stdin, "utf-8-sig"), (sys.stdout, "utf-8"), (sys.stderr, "utf-8")):
|
|
15
|
+
if hasattr(stream, "reconfigure"):
|
|
16
|
+
stream.reconfigure(encoding=encoding, errors="strict")
|
|
17
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
18
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
19
|
+
parser.add_argument('--dependencies', required=True, type=Path)
|
|
20
|
+
parser.add_argument('--probe', choices=['jsonschema'])
|
|
21
|
+
if '--script' in argv:
|
|
22
|
+
index = argv.index('--script')
|
|
23
|
+
if len(argv) <= index + 1:
|
|
24
|
+
parser.error('--script requires a file')
|
|
25
|
+
args = parser.parse_args(argv[:index])
|
|
26
|
+
script, tail = Path(argv[index+1]), argv[index+2:]
|
|
27
|
+
else:
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
script, tail = None, []
|
|
30
|
+
dependencies = args.dependencies.resolve(strict=True)
|
|
31
|
+
if not dependencies.is_dir():
|
|
32
|
+
parser.error('dependencies must be a directory')
|
|
33
|
+
sys.dont_write_bytecode = True
|
|
34
|
+
sys.path.insert(0, str(dependencies))
|
|
35
|
+
os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
|
|
36
|
+
os.environ['AGENT_BIOS_PYTHON_EXECUTABLE'] = str(Path(sys.executable).resolve())
|
|
37
|
+
os.environ['AGENT_BIOS_PYTHON_ENTRY'] = str(Path(__file__).resolve())
|
|
38
|
+
os.environ['AGENT_BIOS_PYTHON_DEPS'] = str(dependencies)
|
|
39
|
+
if args.probe:
|
|
40
|
+
module = importlib.import_module(args.probe)
|
|
41
|
+
getattr(module, 'Draft202012Validator')
|
|
42
|
+
print(importlib.metadata.version(args.probe))
|
|
43
|
+
return 0
|
|
44
|
+
if script is None:
|
|
45
|
+
parser.error('a script or probe is required')
|
|
46
|
+
script = script.resolve(strict=True)
|
|
47
|
+
if not script.is_file() or script.suffix != '.py':
|
|
48
|
+
parser.error('script must be a Python file')
|
|
49
|
+
# Isolated Python does not add the script directory to sys.path. Do this only
|
|
50
|
+
# for the explicitly selected application/helper, not a user's working directory.
|
|
51
|
+
sys.path.insert(0, str(script.parent))
|
|
52
|
+
sys.argv = [str(script), *tail]
|
|
53
|
+
runpy.run_path(str(script), run_name='__main__')
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
raise SystemExit(main())
|