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 CHANGED
@@ -1 +1 @@
1
- 1.3.6
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
- # Replace the managed two-line block when the checkout path changes.
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 += 1
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": "1.3.6",
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 # print the release this install is running
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 and the cached knowledge. The `~/.tai` folder stays — on an
204
- npm install, `npm rm -g terminal-ai-helper` removes the package itself.
205
- Restart the terminal afterwards.
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 after == before or _npm_configured():
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
- """Say which release this is."""
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
- print(f"tai {_read_version()}")
797
- return 0
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