@macintacos/caret 1.1.0 → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caret",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Review, annotate, and approve Claude Code plans in a local web UI.",
5
5
  "author": { "name": "macintacos" },
6
6
  "homepage": "https://github.com/macintacos/caret",
package/README.md CHANGED
@@ -60,6 +60,9 @@ bunx --no-cache @macintacos/caret@latest install --refresh # update
60
60
  bunx --no-cache @macintacos/caret@latest install --uninstall # remove
61
61
  ```
62
62
 
63
+ `--refresh` restarts the caret service on the newest caret installed, never older than the
64
+ one you ran; caret keeps a copy of it under `~/.local/state/caret/roots/`.
65
+
63
66
  `caret`'s UI is designed to check for updates, at most once a day. When a newer `caret` is
64
67
  out, the review UI says so once: a toast on load that opens **What's new** (the skipped
65
68
  release notes or trunk commits), a mark on the settings button, and a
@@ -19,6 +19,9 @@
19
19
  # service the launchd label WITHOUT `.plist` on macOS; the full unit name
20
20
  # WITH `.service` on Linux. Only macOS appends a suffix below.
21
21
  #
22
+ # $XDG_STATE_HOME/caret/roots/<version>/ holds caret install's own copies, scanned
23
+ # beside the agents' caches.
24
+ #
22
25
  # Each record must end with a newline: `read -r` reports EOF on a file without
23
26
  # one, and the read is what decides the record exists.
24
27
 
@@ -28,6 +31,8 @@ state="${XDG_STATE_HOME:-$HOME/.local/state}/caret"
28
31
  records="$state/launcher"
29
32
  claude_roots="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/caret/caret"
30
33
  opencode_roots="${XDG_CACHE_HOME:-$HOME/.cache}/opencode/packages/@macintacos"
34
+ # Keep in sync with ownedRootsDir() (src/config/paths.ts).
35
+ owned_roots="$state/roots"
31
36
 
32
37
  # The units name no log destination, so nothing opens this before caret does:
33
38
  # openDaemonStderr's 0700/0600 guarantee (src/daemon/lifecycle.ts) happens here for a
@@ -60,50 +65,70 @@ read_record() {
60
65
  [ -f "$1" ] && read -r value <"$1" && [ -n "$value" ] && printf '%s' "$value"
61
66
  }
62
67
 
63
- # Highest of the `<version><TAB><path>` lines on stdin. A numeric field sort
64
- # rather than `sort -V`, which BSD sort does not carry everywhere. Ceiling: on an
65
- # equal numeric prefix the whole-line tiebreak puts a prerelease ABOVE the stable
66
- # release, so 0.15.0-rc.1 would beat 0.15.0. caret publishes no prereleases.
68
+ # Highest of the `<version><TAB><rest>` lines on stdin, printing <rest>. A numeric
69
+ # field sort rather than `sort -V`, which BSD sort does not carry everywhere. On an
70
+ # equal numeric prefix sort compares the whole line, and two things ride on that:
71
+ # candidates() puts its rank right after the version, so an agent's root (1) beats an
72
+ # owned copy (0) — keep the sort unstable (no -s); and a prerelease sorts ABOVE its
73
+ # release (0.15.0-rc.1 beats 0.15.0), a ceiling since caret publishes no prereleases.
74
+ # Keep in sync with pickLauncherRoot (src/commands/install/launcher.ts).
67
75
  highest() { sort -t. -k1,1n -k2,2n -k3,3n | tail -1 | cut -f2-; }
68
76
 
69
- # A candidate is a caret when it ships an executable bin/caret — the one file
70
- # every install method carries, tested the way it is used, since a non-executable
71
- # one would exec-fail at 126 and the supervisor would respawn into it forever.
77
+ # bin/caret-native embeds its UI at compile time; the bundle serves ui/dist, so a
78
+ # half-installed one would answer every UI request with ENOENT. A non-executable
79
+ # bin/caret would exec-fail at 126 and the supervisor would respawn into it forever.
80
+ # Keep in sync with isRunnableRoot (src/daemon/lifecycle.ts).
81
+ root_runnable() {
82
+ [ -x "$1/bin/caret" ] || return 1
83
+ [ -x "$1/bin/caret-native" ] || [ -f "$1/ui/dist/index.html" ]
84
+ }
85
+
72
86
  # Version comes from package.json, never the directory name: OpenCode names its
73
87
  # dir after the verbatim `plugin` specifier. The key is anchored so a minified
74
88
  # manifest yields nothing rather than a later key's value — a miss falls into the
75
89
  # branch that names the directory, a wrong version picks the wrong caret.
76
90
  candidate_version() {
77
- [ -x "$1/bin/caret" ] || return 1
91
+ root_runnable "$1" || return 1
78
92
  sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1/package.json" 2>/dev/null | head -1
79
93
  }
80
94
 
81
- # Every dir either cache offers, runnable caret or not — the difference between
82
- # a broken install (terminal) and an empty cache (eviction). The OpenCode entry
83
- # is a two-line npm project, not a caret root; the root is one level down.
95
+ # Every dir either agent's cache or caret's own copies offer, runnable caret or
96
+ # not — the difference between a broken install (terminal) and an empty cache
97
+ # (eviction). The OpenCode entry is a two-line npm project, not a caret root; the
98
+ # root is one level down. `*` skips dot-prefixed dirs, so a half-copied owned root
99
+ # is never a candidate. Keep in sync with launcherCandidateDirs
100
+ # (src/commands/install/launcher.ts).
84
101
  candidate_dirs() {
85
102
  local dir
86
- for dir in "$claude_roots"/* "$opencode_roots"/caret*/node_modules/@macintacos/caret; do
103
+ for dir in "$claude_roots"/* "$opencode_roots"/caret*/node_modules/@macintacos/caret \
104
+ "$owned_roots"/*; do
87
105
  if [ -d "$dir" ]; then printf '%s\n' "$dir"; fi
88
106
  done
89
107
  }
90
108
 
109
+ # `<version><TAB><rank><TAB><dir>`: rank 0 for caret's own copy, 1 for an agent's.
91
110
  candidates() {
92
- local dir version
111
+ local dir version rank
93
112
  candidate_dirs | while read -r dir; do
94
113
  version="$(candidate_version "$dir" || true)"
95
- if [ -n "$version" ]; then printf '%s\t%s\n' "$version" "$dir"; fi
114
+ rank=1
115
+ case "$dir" in "$owned_roots"/*) rank=0 ;; esac
116
+ if [ -n "$version" ]; then printf '%s\t%s\t%s\n' "$version" "$rank" "$dir"; fi
96
117
  done
97
118
  }
98
119
 
99
120
  resolve_root() {
100
121
  local pin
101
122
  pin="$(read_record "$records/pinned-root" || true)"
102
- if [ -n "$pin" ] && [ -x "$pin/bin/caret" ]; then
123
+ if [ -n "$pin" ] && root_runnable "$pin"; then
103
124
  printf '%s' "$pin"
104
125
  return 0
105
126
  fi
106
- candidates | highest
127
+ # The rank sorts an agent's root above an owned copy of the same version: a hook
128
+ # reuses a supervised daemon only when its build matches. highest() leaves
129
+ # candidates()' rank in front of the dir. Keep in sync with pickLauncherRoot
130
+ # (src/commands/install/launcher.ts).
131
+ candidates | highest | cut -f2-
107
132
  }
108
133
 
109
134
  # The newest mise-managed bun, for a machine whose mise shims are not on the
@@ -153,8 +178,8 @@ stop_agent() {
153
178
  # tears its service down and deletes itself. Returns non-zero without touching
154
179
  # anything when there is no `service` record — nothing here installed a
155
180
  # supervisor, so this launcher cannot know what still names it, and deleting
156
- # itself would leave whatever does respawning on ENOENT. Deleting only bin/ and
157
- # the records leaves review state for a reinstall.
181
+ # itself would leave whatever does respawning on ENOENT. Deleting bin/, the
182
+ # records and caret's own roots leaves review state for a reinstall.
158
183
  evict() {
159
184
  local service unit
160
185
  service="$(read_record "$records/service" || true)"
@@ -166,17 +191,17 @@ evict() {
166
191
  systemctl --user disable --now "$service" || true
167
192
  unit="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$service"
168
193
  fi
169
- printf 'caret-launcher: no caret installed anywhere; removing %s and %s.\n' \
170
- "$unit" "$state/bin" >&2
194
+ printf 'caret-launcher: no caret installed anywhere; removing %s and %s/{bin,launcher,roots}.\n' \
195
+ "$unit" "$state" >&2
171
196
  rm -f "$unit"
172
- rm -rf "${state:?}/bin" "${state:?}/launcher"
197
+ rm -rf "${state:?}/bin" "${state:?}/launcher" "${state:?}/roots"
173
198
  exit 0
174
199
  }
175
200
 
176
201
  # Only under a supervisor: a hand-run launcher keeps its diagnostics on the terminal.
177
202
  if [ "${CARET_SUPERVISED:-}" = 1 ]; then open_daemon_log; fi
178
203
 
179
- # Only a total miss re-probes. A populated version dir with no runnable bin/caret
204
+ # Only a total miss re-probes. A populated version dir with no runnable caret
180
205
  # is far more likely a bad install than an extraction caught mid-flight, and that
181
206
  # path is recoverable with `caret install`; an empty cache is the one whose branch
182
207
  # deletes things, so it is the one worth waiting on.
@@ -190,7 +215,7 @@ done
190
215
  if [ -z "$root" ]; then
191
216
  found="$(candidate_dirs)"
192
217
  if [ -z "$found" ]; then evict || true; fi
193
- printf 'caret-launcher: no runnable bin/caret under any of:\n%s\n' \
218
+ printf 'caret-launcher: no runnable caret (an executable bin/caret and a UI build) under any of:\n%s\n' \
194
219
  "${found:-(no install directories at all)}" >&2
195
220
  printf 'caret-launcher: run: caret install — to restore the service.\n' >&2
196
221
  stop_agent
@@ -39,7 +39,8 @@ one. What each id means:
39
39
 
40
40
  | id | what failed | relay |
41
41
  | --- | --- | --- |
42
- | `daemon-reachable` | a caret service is recorded but the daemon did not answer, or the effective port answers as something other than caret | the check's remedy verbatim, then offer to read any log it names |
42
+ | `daemon-reachable` | a caret service is recorded but its supervisor is not keeping it alive (not loaded, turned off, or parked) or the daemon did not answer; or the effective port answers as something other than caret | the check's remedy verbatim, then offer to read any log it names |
43
+ | `daemon-version` | the running daemon is older than npm's latest | the check's remedy verbatim |
43
44
  | `daemon-lock` | the lock names a dead pid, or a port caret is not configured to bind | the check's remedy verbatim |
44
45
  | `agent-install` | the agent the check names does not have caret enabled | `caret install` |
45
46
  | `log-errors` | a live log wrote an NDJSON error record in the last 24h; the detail names which log and when it last erred | step 4 |