terminal-ai-helper 1.3.6 → 2.0.1
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/VERSION +1 -1
- package/install.sh +36 -4
- package/package.json +2 -2
- package/readme.md +9 -5
- package/tai/cli.py +102 -4
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.0.1
|
package/install.sh
CHANGED
|
@@ -142,12 +142,12 @@ _tai_enable() {
|
|
|
142
142
|
local rc="$1" plugin="$2" source_line="$3" plugin_name
|
|
143
143
|
plugin_name="${plugin##*/}"
|
|
144
144
|
mkdir -p "$(dirname "$rc")"
|
|
145
|
-
python3 - "$rc" "$source_line" "$plugin_name" <<'PY'
|
|
145
|
+
python3 - "$rc" "$source_line" "$plugin_name" "$plugin" <<'PY'
|
|
146
146
|
import os
|
|
147
147
|
import sys
|
|
148
148
|
from pathlib import Path
|
|
149
149
|
|
|
150
|
-
rc, source_line, plugin_name = sys.argv[1:]
|
|
150
|
+
rc, source_line, plugin_name, ours = sys.argv[1:]
|
|
151
151
|
path = Path(rc)
|
|
152
152
|
# surrogateescape, not errors="replace": a dotfile in a legacy encoding has bytes
|
|
153
153
|
# that are not valid UTF-8, and replacing them on read would silently rewrite the
|
|
@@ -156,18 +156,47 @@ text = path.read_text(encoding="utf-8", errors="surrogateescape") if path.exists
|
|
|
156
156
|
marker = "# tai autocomplete (installed by ./install.sh)"
|
|
157
157
|
lines = text.splitlines(keepends=True)
|
|
158
158
|
cleaned = []
|
|
159
|
+
retargeted = []
|
|
159
160
|
i = 0
|
|
160
161
|
while i < len(lines):
|
|
161
162
|
line = lines[i]
|
|
162
163
|
if line.strip() == marker:
|
|
163
|
-
#
|
|
164
|
+
# The managed block: refresh its line in place rather than removing and
|
|
165
|
+
# re-appending, so the block keeps its place and a moved tree rewrites
|
|
166
|
+
# the one line instead of gaining a second one.
|
|
167
|
+
cleaned.append(line)
|
|
164
168
|
i += 1
|
|
165
169
|
if i < len(lines) and plugin_name in lines[i] and "source" in lines[i]:
|
|
166
|
-
i
|
|
170
|
+
lines[i] = source_line + "\n"
|
|
167
171
|
continue
|
|
172
|
+
stripped = line.lstrip()
|
|
173
|
+
if (not stripped.startswith("#") and "source" in line
|
|
174
|
+
and "plugins/" in line and plugin_name in line
|
|
175
|
+
and ours not in line):
|
|
176
|
+
# A tai source line left by an older door: the same plugin name from
|
|
177
|
+
# another tree — the checkout the curl one-liner cloned, still in the
|
|
178
|
+
# rc after npm took over the wrapper. Matching by name alone said
|
|
179
|
+
# "already enabled" and kept the shell sourcing code npm's updates
|
|
180
|
+
# never touch: `tai version` reported one tree while the shell ran
|
|
181
|
+
# another. The line is this install's to own, so it is retargeted.
|
|
182
|
+
old = [t for t in line.split() if plugin_name in t]
|
|
183
|
+
retargeted.append(old[-1].strip("\"'") if old else line.strip())
|
|
184
|
+
line = source_line + "\n"
|
|
168
185
|
cleaned.append(line)
|
|
169
186
|
i += 1
|
|
170
187
|
|
|
188
|
+
# A retarget can meet a stale line twice (a managed block and a stray copy);
|
|
189
|
+
# the line is sourced once, first occurrence wins.
|
|
190
|
+
wanted = source_line + "\n"
|
|
191
|
+
deduped, seen = [], False
|
|
192
|
+
for line in cleaned:
|
|
193
|
+
if line == wanted:
|
|
194
|
+
if seen:
|
|
195
|
+
continue
|
|
196
|
+
seen = True
|
|
197
|
+
deduped.append(line)
|
|
198
|
+
cleaned = deduped
|
|
199
|
+
|
|
171
200
|
# A manually configured integration is already enabled; do not add a second
|
|
172
201
|
# copy when the installer is rerun.
|
|
173
202
|
has_source = any(
|
|
@@ -180,6 +209,9 @@ if not has_source:
|
|
|
180
209
|
cleaned.append(marker + "\n")
|
|
181
210
|
cleaned.append(source_line + "\n")
|
|
182
211
|
|
|
212
|
+
for old in retargeted:
|
|
213
|
+
print(f"tai: retargeted the {plugin_name} source line ({old} — was another install's tree)")
|
|
214
|
+
|
|
183
215
|
# Replaced atomically, the same rule the index builder follows. This file is the
|
|
184
216
|
# user's shell: an interrupt between truncate and write would otherwise leave a
|
|
185
217
|
# half-written .zshrc and a shell that cannot start.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-ai-helper",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "The terminal that knows your next command Bash/Zsh suggestions that learn from your own history Offline and private with no account required",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,4 +36,4 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"vitepress": "^1.6.3"
|
|
38
38
|
}
|
|
39
|
-
}
|
|
39
|
+
}
|
package/readme.md
CHANGED
|
@@ -57,7 +57,10 @@ history, and builds the first index:
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
That last line matters: a terminal that is already open keeps the old
|
|
60
|
-
version until you open a new one — same after `tai update`.
|
|
60
|
+
version until you open a new one — same after `tai update`. An install
|
|
61
|
+
that switched doors could leave the shell's `source` line naming the
|
|
62
|
+
older tree — code updates never reach it; `tai update` retargets that
|
|
63
|
+
line, and `tai version` says where it points.
|
|
61
64
|
|
|
62
65
|
## Commands
|
|
63
66
|
|
|
@@ -69,7 +72,7 @@ tai doctor # what is indexed, and what is held back
|
|
|
69
72
|
tai bench # build time, latency, memory, each shell's index read time
|
|
70
73
|
tai web # a read-only localhost dashboard (alias: tai dashboard)
|
|
71
74
|
tai eval # candidate coverage against your own history
|
|
72
|
-
tai version #
|
|
75
|
+
tai version # this install's release, and where the shell plugin is wired
|
|
73
76
|
tai purge # drop unusable rows, then rebuild
|
|
74
77
|
tai forget git sta # drop every stored row for a command, exactly as typed
|
|
75
78
|
tai uninstall # remove everything tai added
|
|
@@ -200,9 +203,10 @@ rc file.
|
|
|
200
203
|
|
|
201
204
|
`tai update` stops rather than overwrite a copy you have edited yourself.
|
|
202
205
|
`tai uninstall` removes the shell integration, the wrapper, the indexes, the
|
|
203
|
-
SQLite history
|
|
204
|
-
|
|
205
|
-
Restart the terminal
|
|
206
|
+
SQLite history, the cached knowledge and the `~/.tai` checkout itself; a
|
|
207
|
+
folder that is not a tai checkout is named and left alone. On an npm install
|
|
208
|
+
it also runs `npm rm -g terminal-ai-helper` itself. Restart the terminal
|
|
209
|
+
afterwards.
|
|
206
210
|
|
|
207
211
|
## Why it stays fast
|
|
208
212
|
|
package/tai/cli.py
CHANGED
|
@@ -213,6 +213,15 @@ def _update_npm(repo: "Path") -> int:
|
|
|
213
213
|
The allow-scripts flag rides along because npm's newer vintages block
|
|
214
214
|
install scripts by default — the same gate the readme's install line
|
|
215
215
|
opens — and a blocked hook would leave a replaced tree nobody wired.
|
|
216
|
+
|
|
217
|
+
The rerun is decided by the wiring, not by the version. An install that
|
|
218
|
+
switched doors could leave the rc line naming an older tree — the
|
|
219
|
+
checkout the curl one-liner cloned, still sourced after npm took over
|
|
220
|
+
the wrapper — and then every `tai update` reported a version the shell
|
|
221
|
+
was not running, "already up to date" being exactly when the old guard
|
|
222
|
+
(`after == before or configured`) skipped the repair. The installer is
|
|
223
|
+
idempotent and its configured check is the wiring's own test, so it runs
|
|
224
|
+
whenever that test fails, whichever the version says.
|
|
216
225
|
"""
|
|
217
226
|
import shutil
|
|
218
227
|
import subprocess
|
|
@@ -233,7 +242,7 @@ def _update_npm(repo: "Path") -> int:
|
|
|
233
242
|
# progress, and this line is the verdict.
|
|
234
243
|
print("✓ already up to date" if after == before else f"✓ updated to {after}",
|
|
235
244
|
flush=True)
|
|
236
|
-
if
|
|
245
|
+
if _npm_configured():
|
|
237
246
|
return 0
|
|
238
247
|
installer = repo / "install.sh"
|
|
239
248
|
if not installer.exists():
|
|
@@ -505,6 +514,7 @@ def cmd_eval_jev(a) -> int:
|
|
|
505
514
|
|
|
506
515
|
def cmd_uninstall(a) -> int:
|
|
507
516
|
import shutil
|
|
517
|
+
import subprocess
|
|
508
518
|
from pathlib import Path
|
|
509
519
|
|
|
510
520
|
from tai.paths import data_dir
|
|
@@ -519,7 +529,17 @@ def cmd_uninstall(a) -> int:
|
|
|
519
529
|
# BIN_DIR is the one that gets removed.
|
|
520
530
|
wrapper = Path(os.environ.get("BIN_DIR") or home / ".local" / "bin") / "tai"
|
|
521
531
|
removed = []
|
|
532
|
+
npm_door = False
|
|
522
533
|
if wrapper.exists() or wrapper.is_symlink():
|
|
534
|
+
# The wrapper names the checkout it execs, and that name is the door:
|
|
535
|
+
# a wrapper pointing into node_modules means the package itself is
|
|
536
|
+
# still installed, and the uninstall is not done until npm has it
|
|
537
|
+
# back. Read before unlink; a wrapper that cannot be read still goes.
|
|
538
|
+
try:
|
|
539
|
+
npm_door = ("node_modules/terminal-ai-helper"
|
|
540
|
+
in wrapper.read_text(errors="replace"))
|
|
541
|
+
except OSError:
|
|
542
|
+
pass
|
|
523
543
|
wrapper.unlink()
|
|
524
544
|
removed.append(str(wrapper))
|
|
525
545
|
if data.exists():
|
|
@@ -565,9 +585,47 @@ def cmd_uninstall(a) -> int:
|
|
|
565
585
|
tmp.write_text(new, encoding="utf-8", errors="surrogateescape")
|
|
566
586
|
os.replace(tmp, rc)
|
|
567
587
|
removed.append(str(rc))
|
|
588
|
+
# The piped door clones the code itself to TAI_DIR (default ~/.tai), and
|
|
589
|
+
# leaving it behind would leave the program installed in every way but the
|
|
590
|
+
# wrapper — the same rule the rc blocks above are held to. What makes the
|
|
591
|
+
# directory tai's is its layout: cli.py and the installer inside it. A
|
|
592
|
+
# directory without that shape is not ours to delete, so it is named and
|
|
593
|
+
# kept. Last on purpose: this command usually runs from inside the checkout
|
|
594
|
+
# (the wrapper execs this very file), so removing it has to be the last
|
|
595
|
+
# thing the run does.
|
|
596
|
+
checkout = Path(os.environ.get("TAI_DIR") or home / ".tai")
|
|
597
|
+
left = None
|
|
598
|
+
if checkout.exists():
|
|
599
|
+
if ((checkout / "tai" / "cli.py").exists()
|
|
600
|
+
and (checkout / "install.sh").exists()):
|
|
601
|
+
shutil.rmtree(checkout)
|
|
602
|
+
removed.append(str(checkout))
|
|
603
|
+
else:
|
|
604
|
+
left = checkout
|
|
605
|
+
# The npm door's code is npm's to remove, and the running tree says the
|
|
606
|
+
# same thing the wrapper did — this process may itself be executing from
|
|
607
|
+
# the package npm owns. Last, for the same reason the checkout is: the
|
|
608
|
+
# tree this command runs from can be the thing being removed.
|
|
609
|
+
npm_note = ""
|
|
610
|
+
if npm_door or _install_method(_repo_dir()) == "npm":
|
|
611
|
+
npm = shutil.which("npm")
|
|
612
|
+
if npm:
|
|
613
|
+
done = subprocess.run([npm, "rm", "-g", "terminal-ai-helper"],
|
|
614
|
+
capture_output=True, text=True)
|
|
615
|
+
if done.returncode == 0:
|
|
616
|
+
removed.append("terminal-ai-helper (npm global package)")
|
|
617
|
+
else:
|
|
618
|
+
npm_note = done.stderr.strip()
|
|
619
|
+
else:
|
|
620
|
+
npm_note = ("npm is not on PATH — the package is removed with: "
|
|
621
|
+
"npm rm -g terminal-ai-helper")
|
|
568
622
|
print("tai removed" if removed else "tai was not installed")
|
|
569
623
|
for path in removed:
|
|
570
624
|
print(f" removed {path}")
|
|
625
|
+
if left:
|
|
626
|
+
print(f" left {left} — not a tai checkout")
|
|
627
|
+
if npm_note:
|
|
628
|
+
print(f" {npm_note}")
|
|
571
629
|
return 0
|
|
572
630
|
|
|
573
631
|
|
|
@@ -645,9 +703,47 @@ def cmd_jev(a) -> int:
|
|
|
645
703
|
|
|
646
704
|
|
|
647
705
|
def cmd_version(a) -> int:
|
|
648
|
-
"""
|
|
706
|
+
"""Which release this is, and where the shell's plugin line points.
|
|
707
|
+
|
|
708
|
+
The second half exists because the two can disagree. An install that
|
|
709
|
+
switched doors — the curl one-liner's checkout replaced by the npm
|
|
710
|
+
package — left the rc line naming the older tree, and the shell went on
|
|
711
|
+
running code `tai version` no longer reported: the release number comes
|
|
712
|
+
from the tree the wrapper execs, the plugin comes from wherever the rc
|
|
713
|
+
says. Both facts are read here, so the mismatch is visible in the same
|
|
714
|
+
command a user runs to answer "which tai is this".
|
|
715
|
+
"""
|
|
649
716
|
version = _read_version()
|
|
650
717
|
print(f"tai {version}")
|
|
718
|
+
from pathlib import Path
|
|
719
|
+
repo = _repo_dir()
|
|
720
|
+
home = Path.home()
|
|
721
|
+
for shell, rc_name, plugin in (("zsh", ".zshrc", "tai.zsh"),
|
|
722
|
+
("bash", ".bashrc", "tai.bash")):
|
|
723
|
+
wired = ""
|
|
724
|
+
try:
|
|
725
|
+
for line in (home / rc_name).read_text(
|
|
726
|
+
encoding="utf-8", errors="surrogateescape").splitlines():
|
|
727
|
+
if line.lstrip().startswith("#") or "source" not in line \
|
|
728
|
+
or plugin not in line:
|
|
729
|
+
continue
|
|
730
|
+
tokens = [t for t in line.split() if plugin in t]
|
|
731
|
+
if tokens:
|
|
732
|
+
wired = tokens[-1].strip("\"'")
|
|
733
|
+
break
|
|
734
|
+
except OSError:
|
|
735
|
+
pass
|
|
736
|
+
if not wired:
|
|
737
|
+
print(f"{shell} plugin not wired")
|
|
738
|
+
continue
|
|
739
|
+
note = ""
|
|
740
|
+
try:
|
|
741
|
+
if Path(wired).expanduser().resolve() != \
|
|
742
|
+
(repo / "plugins" / plugin).resolve():
|
|
743
|
+
note = " — another install; 'tai update' rewires it here"
|
|
744
|
+
except OSError:
|
|
745
|
+
pass
|
|
746
|
+
print(f"{shell} plugin {wired}{note}")
|
|
651
747
|
return 0
|
|
652
748
|
|
|
653
749
|
|
|
@@ -793,8 +889,10 @@ def main() -> int:
|
|
|
793
889
|
if argv and argv[0] == "record" and "--help" not in argv and "-h" not in argv:
|
|
794
890
|
return cmd_record(_fast_record(argv[1:]))
|
|
795
891
|
if argv == ["version"]:
|
|
796
|
-
|
|
797
|
-
|
|
892
|
+
# The report is two small rc reads beside the version file — the lean
|
|
893
|
+
# path can afford what the parser path would print, and both doors
|
|
894
|
+
# must say the same thing about the wiring.
|
|
895
|
+
return cmd_version(None)
|
|
798
896
|
# Any other command is a tai process the user asked for, and a non-empty
|
|
799
897
|
# spool is records waiting for one: drain it here, so `tai suggest`,
|
|
800
898
|
# `tai refresh` or the dashboard never answer from a store that is a few
|