aiscript 1.0.0__tar.gz → 1.1.0__tar.gz
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.
- {aiscript-1.0.0 → aiscript-1.1.0}/PKG-INFO +47 -7
- {aiscript-1.0.0 → aiscript-1.1.0}/README.md +45 -5
- {aiscript-1.0.0 → aiscript-1.1.0}/pyproject.toml +2 -2
- {aiscript-1.0.0 → aiscript-1.1.0}/pyproject.toml.orig +2 -2
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/__init__.py +1 -1
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/builder.py +30 -8
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/cli.py +6 -3
- aiscript-1.1.0/src/aiscript/env.py +120 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/listing.py +28 -11
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/llm.py +189 -36
- aiscript-1.1.0/src/aiscript/paths.py +131 -0
- aiscript-1.1.0/src/aiscript/pysetup.py +191 -0
- aiscript-1.1.0/src/aiscript/reviewer.py +925 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/runner.py +11 -1
- aiscript-1.0.0/src/aiscript/paths.py +0 -77
- aiscript-1.0.0/src/aiscript/reviewer.py +0 -491
- {aiscript-1.0.0 → aiscript-1.1.0}/LICENSE +0 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/__main__.py +0 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/config.py +0 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/errors.py +0 -0
- {aiscript-1.0.0 → aiscript-1.1.0}/src/aiscript/init_cmd.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiscript
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM.
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM.
|
|
5
5
|
Author: Daniel Illescas Romero
|
|
6
6
|
Author-email: Daniel Illescas Romero <illescas.daniel@protonmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -17,8 +17,10 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
[](https://pypi.org/project/aiscript/)
|
|
18
18
|
[](https://pypi.org/project/aiscript/)
|
|
19
19
|
|
|
20
|
-
Describe what you want in plain English and `aiscript` asks an LLM to write a
|
|
21
|
-
|
|
20
|
+
Describe what you want in plain English and `aiscript` asks an LLM to write a script, caches
|
|
21
|
+
it, and runs it. The model picks the language: your native shell (Bash on Linux/macOS, PowerShell
|
|
22
|
+
on Windows) or Python when that fits better. Python scripts that need third-party packages get
|
|
23
|
+
their own virtual environment. The next run is instant:
|
|
22
24
|
no network, no config. Before anything risky is saved or run, every generated script is checked
|
|
23
25
|
by a built-in static scan and, optionally, by a decision model: [Jev](https://docs.typesafe.ai)
|
|
24
26
|
by TypeSafe AI (hosted) or a local model served by [Ollaya](https://ollaya.dev). Risky scripts
|
|
@@ -34,7 +36,19 @@ pipx install aiscript
|
|
|
34
36
|
pip install aiscript
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Requires Python 3.10+.
|
|
39
|
+
Requires Python 3.10+.
|
|
40
|
+
|
|
41
|
+
To upgrade to the latest version, use the same tool you installed with:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv tool upgrade aiscript
|
|
45
|
+
# or
|
|
46
|
+
pipx upgrade aiscript
|
|
47
|
+
# or
|
|
48
|
+
pip install --upgrade aiscript
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Your config, prompts and cached scripts in `~/.config/aiscript/` are kept. To work on aiscript itself, see [docs/development.md](docs/development.md).
|
|
38
52
|
|
|
39
53
|
## Quickstart
|
|
40
54
|
|
|
@@ -135,17 +149,43 @@ variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration
|
|
|
135
149
|
|
|
136
150
|
Command names must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
137
151
|
|
|
152
|
+
## Shell or Python
|
|
153
|
+
|
|
154
|
+
The model is told your OS, architecture, native shell, the Python version it will run under and
|
|
155
|
+
whether [uv](https://docs.astral.sh/uv/) is installed, and chooses the language: the shell for thin
|
|
156
|
+
wrappers around CLI tools and pipelines, Python for parsing JSON/CSV/HTML, data processing, HTTP
|
|
157
|
+
APIs and anything that would be fragile in shell. Say "in python" or "as a shell script" in your
|
|
158
|
+
description to ask for one; the model follows it unless it is clearly a poor fit (and then notes
|
|
159
|
+
why in the script header). `aiscript --list` shows each command's language.
|
|
160
|
+
|
|
161
|
+
- **Standard-library Python** is saved as `functions/NAME.py` and run with the system `python3`
|
|
162
|
+
(`py -3` on Windows), or with aiscript's own interpreter if there is none.
|
|
163
|
+
- **Python with dependencies** declares them in an inline
|
|
164
|
+
[PEP 723](https://peps.python.org/pep-0723/) block. After you approve (any dependency requires a
|
|
165
|
+
`[y/N]` approval that lists the packages), aiscript creates `functions/NAME/` with `main.py` and
|
|
166
|
+
a private `.venv`: with uv (`pyproject.toml` + `uv.lock`, `uv sync`) or otherwise with
|
|
167
|
+
`python -m venv` + `pip install -r requirements.txt`. The script always runs with that venv's
|
|
168
|
+
interpreter, so later runs are offline. A rebuild with the same dependencies reuses the venv; a
|
|
169
|
+
failed install keeps the previous version. Without uv, Debian/Ubuntu need `python3-venv`.
|
|
170
|
+
|
|
138
171
|
## How it works
|
|
139
172
|
|
|
140
173
|
```
|
|
141
174
|
~/.config/aiscript/ # or $XDG_CONFIG_HOME/aiscript, or $AISCRIPT_HOME
|
|
142
175
|
├── config.yaml # providers and keys
|
|
143
176
|
├── prompts/<name>.txt # your intent for each command
|
|
144
|
-
└── functions
|
|
177
|
+
└── functions/
|
|
178
|
+
├── <name>.sh # generated, reviewed scripts (.ps1 on Windows)
|
|
179
|
+
├── <name>.py # standard-library Python
|
|
180
|
+
└── <name>/ # Python with dependencies
|
|
181
|
+
├── main.py
|
|
182
|
+
├── .venv/
|
|
183
|
+
└── pyproject.toml + uv.lock (or requirements.txt)
|
|
145
184
|
```
|
|
146
185
|
|
|
147
186
|
The first run of `NAME` saves your description to `prompts/NAME.txt`, asks the LLM for a script,
|
|
148
|
-
reviews it, and stores it in `functions
|
|
187
|
+
reviews it, and stores it in `functions/` (only one build per command: switching language
|
|
188
|
+
replaces the old one). Later runs just execute the cached script. If the
|
|
149
189
|
prompt is newer than the script (after `--edit` or a manual change), the script is rebuilt on the
|
|
150
190
|
next run. A `--rebuild` that fails or is rejected keeps the previous script.
|
|
151
191
|
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
[](https://pypi.org/project/aiscript/)
|
|
4
4
|
[](https://pypi.org/project/aiscript/)
|
|
5
5
|
|
|
6
|
-
Describe what you want in plain English and `aiscript` asks an LLM to write a
|
|
7
|
-
|
|
6
|
+
Describe what you want in plain English and `aiscript` asks an LLM to write a script, caches
|
|
7
|
+
it, and runs it. The model picks the language: your native shell (Bash on Linux/macOS, PowerShell
|
|
8
|
+
on Windows) or Python when that fits better. Python scripts that need third-party packages get
|
|
9
|
+
their own virtual environment. The next run is instant:
|
|
8
10
|
no network, no config. Before anything risky is saved or run, every generated script is checked
|
|
9
11
|
by a built-in static scan and, optionally, by a decision model: [Jev](https://docs.typesafe.ai)
|
|
10
12
|
by TypeSafe AI (hosted) or a local model served by [Ollaya](https://ollaya.dev). Risky scripts
|
|
@@ -20,7 +22,19 @@ pipx install aiscript
|
|
|
20
22
|
pip install aiscript
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Requires Python 3.10+.
|
|
25
|
+
Requires Python 3.10+.
|
|
26
|
+
|
|
27
|
+
To upgrade to the latest version, use the same tool you installed with:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv tool upgrade aiscript
|
|
31
|
+
# or
|
|
32
|
+
pipx upgrade aiscript
|
|
33
|
+
# or
|
|
34
|
+
pip install --upgrade aiscript
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Your config, prompts and cached scripts in `~/.config/aiscript/` are kept. To work on aiscript itself, see [docs/development.md](docs/development.md).
|
|
24
38
|
|
|
25
39
|
## Quickstart
|
|
26
40
|
|
|
@@ -121,17 +135,43 @@ variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration
|
|
|
121
135
|
|
|
122
136
|
Command names must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
123
137
|
|
|
138
|
+
## Shell or Python
|
|
139
|
+
|
|
140
|
+
The model is told your OS, architecture, native shell, the Python version it will run under and
|
|
141
|
+
whether [uv](https://docs.astral.sh/uv/) is installed, and chooses the language: the shell for thin
|
|
142
|
+
wrappers around CLI tools and pipelines, Python for parsing JSON/CSV/HTML, data processing, HTTP
|
|
143
|
+
APIs and anything that would be fragile in shell. Say "in python" or "as a shell script" in your
|
|
144
|
+
description to ask for one; the model follows it unless it is clearly a poor fit (and then notes
|
|
145
|
+
why in the script header). `aiscript --list` shows each command's language.
|
|
146
|
+
|
|
147
|
+
- **Standard-library Python** is saved as `functions/NAME.py` and run with the system `python3`
|
|
148
|
+
(`py -3` on Windows), or with aiscript's own interpreter if there is none.
|
|
149
|
+
- **Python with dependencies** declares them in an inline
|
|
150
|
+
[PEP 723](https://peps.python.org/pep-0723/) block. After you approve (any dependency requires a
|
|
151
|
+
`[y/N]` approval that lists the packages), aiscript creates `functions/NAME/` with `main.py` and
|
|
152
|
+
a private `.venv`: with uv (`pyproject.toml` + `uv.lock`, `uv sync`) or otherwise with
|
|
153
|
+
`python -m venv` + `pip install -r requirements.txt`. The script always runs with that venv's
|
|
154
|
+
interpreter, so later runs are offline. A rebuild with the same dependencies reuses the venv; a
|
|
155
|
+
failed install keeps the previous version. Without uv, Debian/Ubuntu need `python3-venv`.
|
|
156
|
+
|
|
124
157
|
## How it works
|
|
125
158
|
|
|
126
159
|
```
|
|
127
160
|
~/.config/aiscript/ # or $XDG_CONFIG_HOME/aiscript, or $AISCRIPT_HOME
|
|
128
161
|
├── config.yaml # providers and keys
|
|
129
162
|
├── prompts/<name>.txt # your intent for each command
|
|
130
|
-
└── functions
|
|
163
|
+
└── functions/
|
|
164
|
+
├── <name>.sh # generated, reviewed scripts (.ps1 on Windows)
|
|
165
|
+
├── <name>.py # standard-library Python
|
|
166
|
+
└── <name>/ # Python with dependencies
|
|
167
|
+
├── main.py
|
|
168
|
+
├── .venv/
|
|
169
|
+
└── pyproject.toml + uv.lock (or requirements.txt)
|
|
131
170
|
```
|
|
132
171
|
|
|
133
172
|
The first run of `NAME` saves your description to `prompts/NAME.txt`, asks the LLM for a script,
|
|
134
|
-
reviews it, and stores it in `functions
|
|
173
|
+
reviews it, and stores it in `functions/` (only one build per command: switching language
|
|
174
|
+
replaces the old one). Later runs just execute the cached script. If the
|
|
135
175
|
prompt is newer than the script (after `--edit` or a manual change), the script is rebuilt on the
|
|
136
176
|
next run. A `--rebuild` that fails or is rejected keeps the previous script.
|
|
137
177
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiscript"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM."
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiscript"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM."
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -4,11 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import os
|
|
7
|
+
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
|
-
from aiscript import llm, paths, reviewer
|
|
12
|
+
from aiscript import llm, paths, pysetup, reviewer
|
|
12
13
|
from aiscript.config import Settings
|
|
13
14
|
from aiscript.errors import AiscriptError
|
|
14
15
|
|
|
@@ -44,17 +45,26 @@ def build(name: str, settings: Settings) -> Path:
|
|
|
44
45
|
print(f"Generating {name!r} with {gen.name}/{gen.model}...", file=sys.stderr)
|
|
45
46
|
script = llm.generate_script(gen, name, intent)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if not reviewer.confirm(script, result):
|
|
48
|
+
result = reviewer.review(script.code, script.language, settings.reviewer, script.dependencies)
|
|
49
|
+
if not reviewer.confirm(script.code, result):
|
|
50
50
|
raise AiscriptError("Script rejected; nothing saved.")
|
|
51
51
|
|
|
52
52
|
paths.ensure_dirs()
|
|
53
|
-
|
|
53
|
+
if script.dependencies:
|
|
54
|
+
target = pysetup.install(name, script.code, script.dependencies)
|
|
55
|
+
else:
|
|
56
|
+
target = paths.function_target(name, script.language, has_deps=False)
|
|
57
|
+
_write_file(name, target, script.code)
|
|
58
|
+
_remove_other_artifacts(name, target)
|
|
59
|
+
print(f"Saved {target}", file=sys.stderr)
|
|
60
|
+
return target
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _write_file(name: str, target: Path, code: str) -> None:
|
|
54
64
|
fd, tmp_name = tempfile.mkstemp(dir=paths.functions_dir(), prefix=f".{name}.", suffix=".tmp")
|
|
55
65
|
try:
|
|
56
66
|
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as fh:
|
|
57
|
-
fh.write(
|
|
67
|
+
fh.write(code)
|
|
58
68
|
if not paths.is_windows():
|
|
59
69
|
os.chmod(tmp_name, 0o755)
|
|
60
70
|
os.replace(tmp_name, target)
|
|
@@ -62,5 +72,17 @@ def build(name: str, settings: Settings) -> Path:
|
|
|
62
72
|
with contextlib.suppress(OSError):
|
|
63
73
|
os.unlink(tmp_name)
|
|
64
74
|
raise
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _remove_other_artifacts(name: str, target: Path) -> None:
|
|
78
|
+
"""Drop builds of ``name`` in another language or layout, so only ``target`` remains."""
|
|
79
|
+
for path in paths.function_artifacts(name):
|
|
80
|
+
if path == target or path in target.parents:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
if path.is_dir():
|
|
84
|
+
shutil.rmtree(path)
|
|
85
|
+
else:
|
|
86
|
+
path.unlink()
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
print(f"aiscript: warning: could not remove old build {path}: {exc}", file=sys.stderr)
|
|
@@ -22,7 +22,7 @@ usage: aiscript NAME [args...] run a command (created/built on first use)
|
|
|
22
22
|
aiscript --list list commands
|
|
23
23
|
aiscript --edit NAME edit a command's prompt (rebuilds on next run)
|
|
24
24
|
aiscript --rebuild NAME regenerate a command's script (does not run it)
|
|
25
|
-
aiscript --remove NAME delete a command's prompt and script
|
|
25
|
+
aiscript --remove NAME delete a command's prompt and script(s)
|
|
26
26
|
aiscript --help | --version
|
|
27
27
|
|
|
28
28
|
Everything after NAME is passed untouched to the generated script.
|
|
@@ -92,12 +92,15 @@ def _rebuild(name: str) -> int:
|
|
|
92
92
|
|
|
93
93
|
def _remove(name: str) -> int:
|
|
94
94
|
paths.validate_command_name(name)
|
|
95
|
-
targets = [p for p in (paths.prompt_path(name), paths.
|
|
95
|
+
targets = [p for p in (paths.prompt_path(name), *paths.function_artifacts(name)) if p.exists()]
|
|
96
96
|
if not targets:
|
|
97
97
|
raise AiscriptError(f"No command named {name!r}.")
|
|
98
98
|
for path in targets:
|
|
99
99
|
try:
|
|
100
|
-
path.
|
|
100
|
+
if path.is_dir():
|
|
101
|
+
shutil.rmtree(path)
|
|
102
|
+
else:
|
|
103
|
+
path.unlink()
|
|
101
104
|
except OSError as exc:
|
|
102
105
|
raise AiscriptError(f"Could not remove {path}: {exc}") from exc
|
|
103
106
|
_err(f"Removed {path}")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Host environment detection for the generator prompt and the runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import platform
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_MIN_PYTHON = (3, 8)
|
|
15
|
+
_VERSION_TIMEOUT_SECONDS = 5.0
|
|
16
|
+
_VERSION_RE = re.compile(r"Python (\d+)\.(\d+)\.(\d+)")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class PythonInfo:
|
|
21
|
+
command: list[str] # argv prefix that runs it, e.g. ["/usr/bin/python3"] or ["py", "-3"]
|
|
22
|
+
version: str # e.g. "3.13.1"
|
|
23
|
+
is_system: bool # False when falling back to aiscript's own interpreter (sys.executable)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_windows() -> bool:
|
|
27
|
+
return platform.system() == "Windows"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _candidates() -> list[list[str]]:
|
|
31
|
+
"""Argv prefixes to try, best first (only those whose executable is on PATH)."""
|
|
32
|
+
names = [["py", "-3"], ["python"]] if _is_windows() else [["python3"], ["python"]]
|
|
33
|
+
found: list[list[str]] = []
|
|
34
|
+
for argv in names:
|
|
35
|
+
exe = shutil.which(argv[0])
|
|
36
|
+
if exe:
|
|
37
|
+
found.append([exe, *argv[1:]])
|
|
38
|
+
return found
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def python_command() -> list[str]:
|
|
42
|
+
"""Fast (PATH lookup only, no subprocess): argv prefix for running a stdlib-only script."""
|
|
43
|
+
candidates = _candidates()
|
|
44
|
+
return candidates[0] if candidates else [sys.executable]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _probe(command: list[str]) -> str | None:
|
|
48
|
+
"""Return the ``X.Y.Z`` version of ``command`` if it is a working Python >= 3.8."""
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
[*command, "--version"],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=_VERSION_TIMEOUT_SECONDS,
|
|
55
|
+
check=False,
|
|
56
|
+
)
|
|
57
|
+
except (OSError, subprocess.SubprocessError):
|
|
58
|
+
return None
|
|
59
|
+
if result.returncode != 0: # e.g. the Windows Store stub
|
|
60
|
+
return None
|
|
61
|
+
match = _VERSION_RE.search(f"{result.stdout}\n{result.stderr}")
|
|
62
|
+
if not match:
|
|
63
|
+
return None
|
|
64
|
+
major, minor, patch = (int(g) for g in match.groups())
|
|
65
|
+
if (major, minor) < _MIN_PYTHON:
|
|
66
|
+
return None
|
|
67
|
+
return f"{major}.{minor}.{patch}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@functools.cache
|
|
71
|
+
def find_python() -> PythonInfo:
|
|
72
|
+
"""System Python with its version (runs ``--version``); falls back to sys.executable."""
|
|
73
|
+
for command in _candidates():
|
|
74
|
+
version = _probe(command)
|
|
75
|
+
if version:
|
|
76
|
+
return PythonInfo(command, version, True)
|
|
77
|
+
return PythonInfo([sys.executable], platform.python_version(), False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def uv_path() -> str | None:
|
|
81
|
+
"""Path to the ``uv`` executable, or None."""
|
|
82
|
+
return shutil.which("uv")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _os_description() -> str:
|
|
86
|
+
system = platform.system() or "Unknown"
|
|
87
|
+
desc = f"{system} {platform.release()}".strip()
|
|
88
|
+
if system == "Linux":
|
|
89
|
+
try:
|
|
90
|
+
for line in Path("/etc/os-release").read_text(encoding="utf-8").splitlines():
|
|
91
|
+
if line.startswith("PRETTY_NAME="):
|
|
92
|
+
pretty = line.split("=", 1)[1].strip().strip("\"'")
|
|
93
|
+
desc += f" ({pretty})"
|
|
94
|
+
break
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
elif system == "Darwin":
|
|
98
|
+
mac_version = platform.mac_ver()[0]
|
|
99
|
+
if mac_version:
|
|
100
|
+
desc += f" (macOS {mac_version})"
|
|
101
|
+
return desc
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def describe() -> str:
|
|
105
|
+
"""Multi-line TARGET ENVIRONMENT block for the generator's system prompt."""
|
|
106
|
+
py = find_python()
|
|
107
|
+
if py.is_system:
|
|
108
|
+
shown = " ".join([Path(py.command[0]).name.removesuffix(".exe"), *py.command[1:]])
|
|
109
|
+
python_line = f"{py.version} ({shown})"
|
|
110
|
+
else:
|
|
111
|
+
python_line = f"{py.version} (aiscript's own interpreter; no suitable system Python found)"
|
|
112
|
+
shell = "PowerShell" if _is_windows() else "Bash"
|
|
113
|
+
lines = [
|
|
114
|
+
f"- Host OS: {_os_description()}",
|
|
115
|
+
f"- Architecture: {platform.machine() or 'unknown'}",
|
|
116
|
+
f"- Native shell: {shell}",
|
|
117
|
+
f"- Python: {python_line}",
|
|
118
|
+
f"- uv: {'available' if uv_path() else 'not installed'}",
|
|
119
|
+
]
|
|
120
|
+
return "\n".join(lines)
|
|
@@ -50,7 +50,13 @@ def _command_names() -> list[str]:
|
|
|
50
50
|
if pdir.is_dir():
|
|
51
51
|
names.update(p.stem for p in pdir.glob("*.txt"))
|
|
52
52
|
if fdir.is_dir():
|
|
53
|
-
|
|
53
|
+
for p in fdir.iterdir():
|
|
54
|
+
if p.name.startswith("."):
|
|
55
|
+
continue
|
|
56
|
+
if p.is_file() and p.suffix in (".sh", ".ps1", ".py"):
|
|
57
|
+
names.add(p.stem)
|
|
58
|
+
elif p.is_dir() and (p / "main.py").is_file():
|
|
59
|
+
names.add(p.name)
|
|
54
60
|
valid = []
|
|
55
61
|
for n in names:
|
|
56
62
|
try:
|
|
@@ -61,14 +67,24 @@ def _command_names() -> list[str]:
|
|
|
61
67
|
return sorted(valid, key=str.lower)
|
|
62
68
|
|
|
63
69
|
|
|
70
|
+
def _lang(fpath: Path) -> str:
|
|
71
|
+
"""LANG column: sh, ps1, py or py+deps (project dir); blank when not built."""
|
|
72
|
+
if not fpath.exists():
|
|
73
|
+
return ""
|
|
74
|
+
if paths.is_project_main(fpath):
|
|
75
|
+
return "py+deps"
|
|
76
|
+
return fpath.suffix.lstrip(".")
|
|
77
|
+
|
|
78
|
+
|
|
64
79
|
def list_commands() -> int:
|
|
65
80
|
"""Print a table of commands to stdout; return 0."""
|
|
66
|
-
rows: list[tuple[str, str, str]] = []
|
|
81
|
+
rows: list[tuple[str, str, str, str]] = []
|
|
67
82
|
any_stale = False
|
|
68
83
|
for name in _command_names():
|
|
69
84
|
ppath = paths.prompt_path(name)
|
|
70
85
|
fpath = paths.function_path(name)
|
|
71
86
|
label = name
|
|
87
|
+
lang = _lang(fpath)
|
|
72
88
|
if fpath.exists():
|
|
73
89
|
header = parse_header(fpath)
|
|
74
90
|
desc, usage = header["description"], header["usage"]
|
|
@@ -77,7 +93,7 @@ def list_commands() -> int:
|
|
|
77
93
|
any_stale = True
|
|
78
94
|
else:
|
|
79
95
|
desc, usage = _NOT_BUILT, ""
|
|
80
|
-
rows.append((label, desc, usage))
|
|
96
|
+
rows.append((label, lang, desc, usage))
|
|
81
97
|
|
|
82
98
|
if not rows:
|
|
83
99
|
print("No commands yet. Try: aiscript <name>", file=sys.stderr)
|
|
@@ -85,16 +101,17 @@ def list_commands() -> int:
|
|
|
85
101
|
|
|
86
102
|
cols = shutil.get_terminal_size(fallback=(100, 24)).columns
|
|
87
103
|
name_w = max(len("NAME"), *(len(r[0]) for r in rows))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
desc_w =
|
|
104
|
+
lang_w = max(len("LANG"), *(len(r[1]) for r in rows))
|
|
105
|
+
usage_w = min(max(len("USAGE"), *(len(r[3]) for r in rows)), 40)
|
|
106
|
+
desc_w = max(20, cols - name_w - lang_w - usage_w - 6)
|
|
107
|
+
desc_w = min(desc_w, max(len("DESCRIPTION"), *(len(r[2]) for r in rows)))
|
|
91
108
|
|
|
92
|
-
def fmt(a: str, b: str, c: str) -> str:
|
|
93
|
-
return f"{a:<{name_w}} {b:<{
|
|
109
|
+
def fmt(a: str, b: str, c: str, d: str) -> str:
|
|
110
|
+
return f"{a:<{name_w}} {b:<{lang_w}} {c:<{desc_w}} {d}".rstrip()
|
|
94
111
|
|
|
95
|
-
lines = [fmt("NAME", "DESCRIPTION", "USAGE")]
|
|
96
|
-
for label, desc, usage in rows:
|
|
97
|
-
lines.append(fmt(label, _truncate(desc, desc_w), _truncate(usage, usage_w)))
|
|
112
|
+
lines = [fmt("NAME", "LANG", "DESCRIPTION", "USAGE")]
|
|
113
|
+
for label, lang, desc, usage in rows:
|
|
114
|
+
lines.append(fmt(label, lang, _truncate(desc, desc_w), _truncate(usage, usage_w)))
|
|
98
115
|
print("\n".join(lines))
|
|
99
116
|
if any_stale:
|
|
100
117
|
print(
|