terminal-ai-helper 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.1
1
+ 2.1.0
package/install.sh CHANGED
@@ -23,11 +23,19 @@ else
23
23
  exit 1
24
24
  }
25
25
  if [ -d "$TAI_DIR/.git" ]; then
26
- # The one-liner is also the update path: an existing checkout is pulled
27
- # forward and reinstalled, never cloned over. A pull that cannot happen
28
- # (diverged, offline) fails here, loudly, rather than installing old code.
26
+ # The one-liner is also the update path: an existing checkout is refetched
27
+ # and reinstalled, never cloned over. The checkout is this install's own —
28
+ # nobody develops in it — so the update is one commit deep every time:
29
+ # fetch --depth 1, reset to what origin named, prune what is left behind.
30
+ # A pull here would download every commit made since the checkout was cut,
31
+ # and a checkout that predates --depth 1 would carry its whole history
32
+ # forever. Offline, the fetch fails and set -e stops the install loudly —
33
+ # the old rule stands: never install old code on the strength of a guess.
29
34
  echo "→ updating the checkout at $TAI_DIR…"
30
- git -C "$TAI_DIR" pull --ff-only --quiet
35
+ _branch="$(git -C "$TAI_DIR" rev-parse --abbrev-ref HEAD)"
36
+ git -C "$TAI_DIR" fetch --depth 1 --quiet origin "$_branch"
37
+ git -C "$TAI_DIR" reset --hard --quiet FETCH_HEAD
38
+ git -C "$TAI_DIR" gc --prune=now --quiet 2>/dev/null || true
31
39
  elif [ -e "$TAI_DIR" ]; then
32
40
  echo "tai: $TAI_DIR exists and is not a tai checkout — remove it or set TAI_DIR" >&2
33
41
  exit 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-ai-helper",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
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": {
package/readme.md CHANGED
@@ -28,20 +28,6 @@ curl -fsSL https://raw.githubusercontent.com/mlibre/Terminal-AI-Helper/main/inst
28
28
  exec zsh
29
29
  ```
30
30
 
31
- Or using npm
32
-
33
- ```sh
34
- npm i -g terminal-ai-helper@latest --allow-scripts=terminal-ai-helper
35
- exec zsh
36
- ```
37
-
38
- The flag is npm's: newer npm blocks install scripts by default, and without
39
- it the package unpacks and nothing is wired — no `tai` command, no rc lines.
40
-
41
- The line downloads TAI to `~/.tai` and runs the installer from there; run it
42
- again later and it updates in place. It enables both shells, imports your
43
- history, and builds the first index:
44
-
45
31
  ```text
46
32
  → cloning tai to /home/you/.tai…
47
33
  → learning from your history, building the index…
@@ -62,6 +48,13 @@ that switched doors could leave the shell's `source` line naming the
62
48
  older tree — code updates never reach it; `tai update` retargets that
63
49
  line, and `tai version` says where it points.
64
50
 
51
+ Or using npm
52
+
53
+ ```sh
54
+ npm i -g terminal-ai-helper@latest --allow-scripts=terminal-ai-helper
55
+ exec zsh
56
+ ```
57
+
65
58
  ## Commands
66
59
 
67
60
  ```sh
package/tai/cli.py CHANGED
@@ -266,6 +266,7 @@ def cmd_update(a) -> int:
266
266
 
267
267
  import shutil
268
268
  import subprocess
269
+ from pathlib import Path
269
270
 
270
271
  git = shutil.which("git")
271
272
  if not git:
@@ -281,15 +282,49 @@ def cmd_update(a) -> int:
281
282
  return subprocess.run([git, "-C", str(repo), "rev-parse", "--short", "HEAD"],
282
283
  capture_output=True, text=True).stdout.strip()
283
284
 
285
+ def run_git(*argv: str) -> subprocess.CompletedProcess:
286
+ return subprocess.run([git, "-C", str(repo), *argv],
287
+ capture_output=True, text=True)
288
+
289
+ # The checkout the one-liner cloned is this install's own tree, not a
290
+ # development branch: nobody commits in it, so the update is a fetch one
291
+ # commit deep and a reset to what origin named — the same three commands
292
+ # install.sh's piped door runs. A pull here would download every commit
293
+ # made since the checkout was cut, and one that predates --depth 1 would
294
+ # carry its whole history forever. A checkout anywhere else is somebody's
295
+ # tree — maybe a fork they develop in — and it keeps the careful pull:
296
+ # fast-forward only, a divergence fails loudly rather than resetting
297
+ # anybody's work away.
298
+ managed = (repo.resolve()
299
+ == Path(os.environ.get("TAI_DIR")
300
+ or Path.home() / ".tai").resolve())
301
+
284
302
  before = head()
285
- pull = subprocess.run([git, "-C", str(repo), "pull", "--ff-only"],
286
- capture_output=True, text=True)
287
- if pull.returncode != 0:
288
- # A local edit in the checkout is the usual cause, and git's own message
289
- # says exactly what to do about it, so pass it through rather than guess.
290
- print("tai: git pull failed:")
291
- print((pull.stderr or pull.stdout).strip())
292
- return 1
303
+ if managed:
304
+ branch = run_git("rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
305
+ failed = None
306
+ for argv in (("fetch", "--depth", "1", "--quiet", "origin", branch),
307
+ ("reset", "--hard", "--quiet", "FETCH_HEAD")):
308
+ done = run_git(*argv)
309
+ if done.returncode != 0:
310
+ failed = argv[0]
311
+ break
312
+ if failed:
313
+ print(f"tai: git {failed} failed:")
314
+ print((done.stderr or done.stdout).strip())
315
+ return 1
316
+ # Hygiene, not a step: what the old history weighed is already
317
+ # unreachable after the reset, and a failed prune costs nothing.
318
+ run_git("gc", "--prune=now", "--quiet")
319
+ else:
320
+ pull = run_git("pull", "--ff-only")
321
+ if pull.returncode != 0:
322
+ # A local edit in the checkout is the usual cause, and git's own
323
+ # message says exactly what to do about it, so pass it through
324
+ # rather than guess.
325
+ print("tai: git pull failed:")
326
+ print((pull.stderr or pull.stdout).strip())
327
+ return 1
293
328
  after = head()
294
329
  # Flushed, because the installer below writes to the same terminal from a
295
330
  # child process and would otherwise appear ahead of this line.
@@ -587,21 +622,29 @@ def cmd_uninstall(a) -> int:
587
622
  removed.append(str(rc))
588
623
  # The piped door clones the code itself to TAI_DIR (default ~/.tai), and
589
624
  # 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.
625
+ # wrapper — the same rule the rc blocks above are held to. The name is the
626
+ # identity: the path is tai's namespace, created by tai's installer and
627
+ # claimed by nothing else, so whatever the checkout has grown into — a
628
+ # shape an older door left half-made, files the user dropped beside the
629
+ # code — goes with the uninstall, and the report says which kind it was.
630
+ # One floor stands: never a directory the home lives in or under, so a
631
+ # mistyped TAI_DIR cannot name more than tai owns. Last on purpose: this
632
+ # command usually runs from inside the checkout (the wrapper execs this
633
+ # very file), so removing it has to be the last thing the run does.
596
634
  checkout = Path(os.environ.get("TAI_DIR") or home / ".tai")
597
- left = None
635
+ held = ""
598
636
  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))
637
+ if home.is_relative_to(checkout):
638
+ held = f"left {checkout} — it holds the home directory"
603
639
  else:
604
- left = checkout
640
+ ours = ((checkout / "tai" / "cli.py").exists()
641
+ and (checkout / "install.sh").exists())
642
+ if checkout.is_dir():
643
+ shutil.rmtree(checkout)
644
+ else:
645
+ checkout.unlink()
646
+ removed.append(str(checkout) + ("" if ours
647
+ else " (not a tai checkout — removed anyway)"))
605
648
  # The npm door's code is npm's to remove, and the running tree says the
606
649
  # same thing the wrapper did — this process may itself be executing from
607
650
  # the package npm owns. Last, for the same reason the checkout is: the
@@ -622,10 +665,10 @@ def cmd_uninstall(a) -> int:
622
665
  print("tai removed" if removed else "tai was not installed")
623
666
  for path in removed:
624
667
  print(f" removed {path}")
625
- if left:
626
- print(f" left {left} — not a tai checkout")
627
668
  if npm_note:
628
669
  print(f" {npm_note}")
670
+ if held:
671
+ print(f" {held}")
629
672
  return 0
630
673
 
631
674