@bigknoxy/hashpilot 4.6.6 → 4.7.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/README.md CHANGED
@@ -175,8 +175,9 @@ silent one.
175
175
  ### Upgrade
176
176
 
177
177
  ```bash
178
- hashpilot upgrade # upgrade to latest from main
178
+ hashpilot upgrade # upgrade to the latest npm release (falls back to GitHub if npm is unreachable)
179
179
  hashpilot upgrade --dry-run # preview what would happen
180
+ hashpilot upgrade --channel some-branch # bleeding-edge: install that exact git branch instead, skipping npm
180
181
  ```
181
182
 
182
183
  ### Uninstall
@@ -357,7 +358,7 @@ diffable, and CI fails on any case that regresses from green.
357
358
 
358
359
  | Command | What It Does |
359
360
  |---------|-------------|
360
- | `upgrade [--dry-run] [--channel <branch>] [--target <dir>] [--keep-telemetry] [--force]` | Upgrade HashPilot from GitHub to latest version |
361
+ | `upgrade [--dry-run] [--channel <branch>] [--target <dir>] [--keep-telemetry] [--force]` | Upgrade HashPilot to the latest version — npm by default, falling back to GitHub; `--channel <branch>` installs that exact git branch instead |
361
362
 
362
363
  ### Edit — Hash Route
363
364
 
@@ -765,7 +765,7 @@ hashpilot doctor [options]
765
765
 
766
766
  #### `upgrade`
767
767
 
768
- Upgrade HashPilot to the latest version from GitHub
768
+ Upgrade HashPilot to the latest version (npm, falling back to GitHub)
769
769
 
770
770
  ```
771
771
  hashpilot upgrade [options]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigknoxy/hashpilot",
3
- "version": "4.6.6",
3
+ "version": "4.7.0",
4
4
  "description": "HashPilot — Global Tool-Agnostic Structured Editing Core for Coding Agents",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,37 +14,233 @@ warn() { printf "${YELLOW}[hashpilot]${NC} %s\n" "$1"; }
14
14
  err() { printf "${RED}[hashpilot]${NC} %s\n" "$1"; }
15
15
  detail() { printf "${DIM} →${NC} %s\n" "$1"; }
16
16
 
17
+ # Single source of truth for the "use npm, not an explicit git ref" sentinel
18
+ # — referenced in this file and (as a literal, since it's a separate process)
19
+ # in src/commands/maintenance.ts's `--channel` default; keep both in sync.
20
+ DEFAULT_CHANNEL="main"
21
+
22
+ # None of this script's network calls bounded how long they'd wait — a host
23
+ # that accepts the TCP connection but never responds (common for corporate
24
+ # proxies blocking a specific destination, which is the exact scenario the
25
+ # npm-registry fallback below exists for) hung the installer indefinitely
26
+ # instead of ever reaching that fallback.
27
+ CURL_META_OPTS=(--connect-timeout 10 --max-time 20)
28
+ CURL_DOWNLOAD_OPTS=(--connect-timeout 10 --max-time 300)
29
+
30
+ # Extract one string field's value from a small JSON blob (grep+sed, no jq
31
+ # dependency, matching this script's existing style) — centralized so every
32
+ # call site gets the same handling instead of each reinventing it slightly
33
+ # differently. Pass "url" as $3 to additionally require the value look like
34
+ # a real http(s) URL: the naive sed substitution only fires on a genuine
35
+ # match, so a non-URL value (empty, relative, a mirror that rewrites the
36
+ # field to something else) would otherwise silently pass the whole
37
+ # grep-matched line through unchanged — still non-empty, so it would pass a
38
+ # bare `-n` check as if it were real.
39
+ json_field() {
40
+ local json="$1" field="$2" require="${3:-}" value
41
+ value=$(echo "$json" | grep -o "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 \
42
+ | sed "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\"/\\1/" || true)
43
+ if [ "$require" = "url" ]; then
44
+ case "$value" in
45
+ https://*|http://*) ;;
46
+ *) value="" ;;
47
+ esac
48
+ fi
49
+ echo "$value"
50
+ }
51
+
52
+ # Download a tarball to a temp file (verifying its sha1 against $3 first, if
53
+ # given — npm's registry metadata includes one for free), then extract it.
54
+ # Shared by the npm and GitHub source fetches below so hardening (timeouts,
55
+ # checksum verification) only has to be added once. Leaves nothing behind
56
+ # and returns non-zero on any failure: download, checksum mismatch, or
57
+ # extraction — every failure mode here is handled identically by the caller
58
+ # (fall back to the next source), so there is no reason for them to differ.
59
+ fetch_and_extract_tarball() {
60
+ local url="$1" dest_dir="$2" expected_sha1="${3:-}" tmp_tarball
61
+ tmp_tarball="$(mktemp)"
62
+ if ! curl -fsSL "${CURL_DOWNLOAD_OPTS[@]}" "$url" -o "$tmp_tarball" 2>/dev/null; then
63
+ rm -f "$tmp_tarball"
64
+ return 1
65
+ fi
66
+ if [ -n "$expected_sha1" ]; then
67
+ local actual_sha1
68
+ # `sha1sum` is GNU coreutils only — stock macOS (BSD userland) has
69
+ # `shasum -a 1` instead, and neither exists on some minimal containers,
70
+ # where `openssl sha1` is the last resort. Checking only for `sha1sum`
71
+ # made every npm-path checksum check fail on macOS, silently forcing
72
+ # every install/upgrade there onto the GitHub fallback — defeating this
73
+ # PR's actual point on a major platform.
74
+ if command -v sha1sum >/dev/null 2>&1; then
75
+ actual_sha1="$(sha1sum "$tmp_tarball" 2>/dev/null | awk '{print $1}')"
76
+ elif command -v shasum >/dev/null 2>&1; then
77
+ actual_sha1="$(shasum -a 1 "$tmp_tarball" 2>/dev/null | awk '{print $1}')"
78
+ elif command -v openssl >/dev/null 2>&1; then
79
+ actual_sha1="$(openssl sha1 "$tmp_tarball" 2>/dev/null | awk '{print $NF}')"
80
+ else
81
+ # Fail closed, not open: forging a match here would silently disable
82
+ # the integrity guarantee this whole check exists for. Returning
83
+ # failure instead routes through this function's normal
84
+ # failure-handling — the caller already treats that identically to a
85
+ # checksum mismatch or a failed download, falling back to the GitHub
86
+ # source rather than installing something unverified.
87
+ warn "no sha1sum/shasum/openssl found; cannot verify tarball checksum"
88
+ rm -f "$tmp_tarball"
89
+ return 1
90
+ fi
91
+ if [ "$actual_sha1" != "$expected_sha1" ]; then
92
+ warn "tarball checksum mismatch (expected ${expected_sha1}, got ${actual_sha1:-<none>})"
93
+ rm -f "$tmp_tarball"
94
+ return 1
95
+ fi
96
+ fi
97
+ if ! tar -xz -C "$dest_dir" --strip-components=1 -f "$tmp_tarball" 2>&1 | while IFS= read -r line; do detail "$line"; done; then
98
+ rm -f "$tmp_tarball"
99
+ return 1
100
+ fi
101
+ rm -f "$tmp_tarball"
102
+ }
103
+
17
104
  # ── Detect source directory ──────────────────────────────────────────────
18
105
  REMOTE_MODE=false
19
106
  SOURCE_DIR=""
107
+ # Declared here (not just inside the remote-mode block below) so the
108
+ # dependency-install step can use it as the authoritative "did this come
109
+ # from npm" signal — local-clone mode and the GitHub-fallback path both
110
+ # leave it false, which is correct for both.
111
+ NPM_INSTALLED=false
112
+
113
+ # An explicit --source wins over everything else, checked here (before the
114
+ # real argument-parsing loop below, which runs too late for this) so that
115
+ # passing --source skips local-clone detection AND the auto-download below
116
+ # entirely — downloading anything when the caller already told us exactly
117
+ # where the source is would be pure waste, and previously caused a real bug:
118
+ # HASHPILOT_VERSION got read from the auto-fetched npm/GitHub tarball, not
119
+ # from the --source directory that was actually installed, silently
120
+ # mislabeling the manifest/version banner whenever the two versions differed.
121
+ # No `break`: --source given twice must resolve to the SAME occurrence the
122
+ # real argument-parsing loop below honors (it keeps the last one), or the
123
+ # version/manifest would be read from one directory while the actual
124
+ # install copies from another — reintroducing the exact class of mismatch
125
+ # this pre-scan exists to prevent.
126
+ EXPLICIT_SOURCE=""
127
+ _ARGV=("$@")
128
+ for ((_i = 0; _i < ${#_ARGV[@]}; _i++)); do
129
+ if [ "${_ARGV[$_i]}" = "--source" ] && [ $((_i + 1)) -lt ${#_ARGV[@]} ]; then
130
+ EXPLICIT_SOURCE="${_ARGV[$((_i + 1))]}"
131
+ fi
132
+ done
20
133
 
134
+ if [ -n "$EXPLICIT_SOURCE" ]; then
135
+ SOURCE_DIR="$EXPLICIT_SOURCE"
21
136
  # Try to resolve from script location (local clone mode)
22
- if SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd 2>/dev/null)"; then
137
+ elif SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd 2>/dev/null)"; then
23
138
  REPO_ROOT="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd 2>/dev/null || echo "")"
24
139
  if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/package.json" ]; then
25
140
  SOURCE_DIR="$REPO_ROOT"
26
141
  fi
27
142
  fi
28
143
 
29
- # No local source — download release tarball from GitHub (curl-pipe / remote mode)
144
+ # No local source — fetch a tarball (curl-pipe / remote mode).
145
+ #
146
+ # Primary source is the published npm package: it's the tested, minimal
147
+ # artifact (no devDependencies, no tests/docs bloat — see
148
+ # tests/packaging.test.ts for what it guarantees ships) instead of the full
149
+ # git source tree, and it stops this installer silently drifting from the
150
+ # npm distribution channel now that publishing actually works (#193). Only
151
+ # curl is used — no `npm`/`node` binary required, since bun is this script's
152
+ # only external prerequisite.
153
+ #
154
+ # Falls back to the GitHub source tarball (release tag, or a branch) when:
155
+ # - the npm registry is unreachable or the package/version can't be found
156
+ # (offline-but-git-reachable environments, corporate proxies that allow
157
+ # github.com but not registry.npmjs.org), or
158
+ # - HASHPILOT_SOURCE_CHANNEL is set to something other than "main" — an
159
+ # explicit non-default channel (e.g. `hashpilot upgrade --channel
160
+ # some-branch`) means the user wants that exact git ref, which npm's
161
+ # published releases can't provide.
162
+ #
163
+ # HASHPILOT_NPM_REGISTRY overrides the registry base URL — used by tests to
164
+ # deterministically force the npm path to fail without relying on a real
165
+ # outage, and by anyone behind an npm registry mirror/proxy.
30
166
  if [ -z "$SOURCE_DIR" ]; then
31
167
  REMOTE_MODE=true
32
168
  CLONE_DIR=$(mktemp -d)
33
-
34
- # Determine version to download (latest release)
35
- log "Fetching latest release info from GitHub..."
36
- RELEASE_INFO=$(curl -fsSL "https://api.github.com/repos/bigknoxy/HashPilot/releases/latest" 2>/dev/null || echo "")
37
- TAG_NAME=$(echo "$RELEASE_INFO" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)
38
- if [ -n "$TAG_NAME" ]; then
39
- TARBALL_URL="https://github.com/bigknoxy/HashPilot/archive/refs/tags/${TAG_NAME}.tar.gz"
40
- log "Downloading HashPilot ${TAG_NAME} from GitHub..."
41
- else
42
- # Fallback to main branch if no release
43
- TARBALL_URL="https://github.com/bigknoxy/HashPilot/archive/refs/heads/main.tar.gz"
44
- log "Downloading HashPilot from main branch..."
169
+ # A stale HASHPILOT_SOURCE_CHANNEL already exported in the caller's shell
170
+ # (or a CI job's environment) must not silently override the channel the
171
+ # user actually asked for on THIS invocation — src/commands/maintenance.ts
172
+ # always sets this explicitly (to "" on the default channel) precisely so
173
+ # `${HASHPILOT_SOURCE_CHANNEL:-$DEFAULT_CHANNEL}` can't see a leftover
174
+ # value from a previous run, but default it defensively here too for
175
+ # anyone invoking install.sh directly rather than through `hashpilot
176
+ # upgrade`.
177
+ SOURCE_CHANNEL="${HASHPILOT_SOURCE_CHANNEL:-$DEFAULT_CHANNEL}"
178
+ [ -z "$SOURCE_CHANNEL" ] && SOURCE_CHANNEL="$DEFAULT_CHANNEL"
179
+ NPM_REGISTRY="${HASHPILOT_NPM_REGISTRY:-https://registry.npmjs.org}"
180
+ NPM_INSTALLED=false
181
+
182
+ if [ "$SOURCE_CHANNEL" = "$DEFAULT_CHANNEL" ]; then
183
+ log "Fetching latest release info from npm..."
184
+ NPM_INFO=$(curl -fsSL "${CURL_META_OPTS[@]}" "${NPM_REGISTRY}/@bigknoxy/hashpilot/latest" 2>/dev/null || echo "")
185
+ NPM_TARBALL_URL="$(json_field "$NPM_INFO" "tarball" url)"
186
+ NPM_VERSION="$(json_field "$NPM_INFO" "version")"
187
+ NPM_SHASUM="$(json_field "$NPM_INFO" "shasum")"
188
+ if [ -n "$NPM_TARBALL_URL" ]; then
189
+ log "Downloading HashPilot v${NPM_VERSION} from npm..."
190
+ # A tarball can download, checksum-verify, and extract cleanly while
191
+ # still being useless — e.g. a registry response that resolved to
192
+ # some unrelated but validly-formed archive. Require a package.json
193
+ # to actually be there before trusting this source; otherwise every
194
+ # later step (the version read right after this block especially)
195
+ # fails with a bare, undiagnosed exit instead of falling back like
196
+ # every other failure mode here does.
197
+ if fetch_and_extract_tarball "$NPM_TARBALL_URL" "$CLONE_DIR" "$NPM_SHASUM"; then
198
+ if [ -f "$CLONE_DIR/package.json" ]; then
199
+ NPM_INSTALLED=true
200
+ else
201
+ warn "npm tarball extracted but had no package.json; falling back to GitHub source"
202
+ fi
203
+ else
204
+ warn "npm tarball download/extract failed; falling back to GitHub source"
205
+ fi
206
+ if [ "$NPM_INSTALLED" = "false" ]; then
207
+ rm -rf "$CLONE_DIR"
208
+ CLONE_DIR=$(mktemp -d)
209
+ fi
210
+ else
211
+ warn "npm registry unreachable or package not found; falling back to GitHub source"
212
+ fi
45
213
  fi
46
-
47
- curl -fsSL "$TARBALL_URL" | tar -xz -C "$CLONE_DIR" --strip-components=1 2>&1 | while IFS= read -r line; do detail "$line"; done
214
+
215
+ if [ "$NPM_INSTALLED" = "false" ]; then
216
+ if [ "$SOURCE_CHANNEL" = "$DEFAULT_CHANNEL" ]; then
217
+ log "Fetching latest release info from GitHub..."
218
+ RELEASE_INFO=$(curl -fsSL "${CURL_META_OPTS[@]}" "https://api.github.com/repos/bigknoxy/HashPilot/releases/latest" 2>/dev/null || echo "")
219
+ TAG_NAME="$(json_field "$RELEASE_INFO" "tag_name")"
220
+ if [ -n "$TAG_NAME" ]; then
221
+ TARBALL_URL="https://github.com/bigknoxy/HashPilot/archive/refs/tags/${TAG_NAME}.tar.gz"
222
+ log "Downloading HashPilot ${TAG_NAME} from GitHub..."
223
+ else
224
+ # Fallback to main branch if no release
225
+ TARBALL_URL="https://github.com/bigknoxy/HashPilot/archive/refs/heads/main.tar.gz"
226
+ log "Downloading HashPilot from main branch..."
227
+ fi
228
+ else
229
+ TARBALL_URL="https://github.com/bigknoxy/HashPilot/archive/refs/heads/${SOURCE_CHANNEL}.tar.gz"
230
+ log "Downloading HashPilot from branch ${SOURCE_CHANNEL}..."
231
+ fi
232
+
233
+ # This is the last fallback — nothing after this if it fails — so an
234
+ # unguarded call here would let `set -e` kill the script with a bare,
235
+ # undiagnosed exit instead of the clear, actionable message every other
236
+ # failure mode in this block already gets.
237
+ if ! fetch_and_extract_tarball "$TARBALL_URL" "$CLONE_DIR"; then
238
+ err "Failed to download or extract HashPilot from ${TARBALL_URL}"
239
+ err "Check your network connection, or that '${SOURCE_CHANNEL}' is a real branch/tag."
240
+ exit 1
241
+ fi
242
+ fi
243
+
48
244
  SOURCE_DIR="$CLONE_DIR"
49
245
  detail "Extracted to $CLONE_DIR"
50
246
  fi
@@ -71,13 +267,18 @@ while [ $# -gt 0 ]; do
71
267
  echo "HashPilot Installer v${HASHPILOT_VERSION}"
72
268
  echo "Usage: $0 [options]"
73
269
  echo " --source <dir> Source directory (default: repo root)."
74
- echo " If omitted and no local source found,"
75
- echo " auto-downloads release tarball from GitHub."
270
+ echo " If omitted and no local source found, auto-downloads"
271
+ echo " from npm (falls back to the GitHub release/main tarball"
272
+ echo " if npm is unreachable)."
76
273
  echo " --target <dir> Install target (default: ~/.agentic-tools)"
77
274
  echo " --keep-telemetry Preserve existing telemetry on reinstall"
78
275
  echo ' --force, -f Overwrite existing install without any prompt (including the non-interactive existing-install notice)'
79
276
  echo " --help, -h Show this help"
80
277
  echo ""
278
+ echo "Env vars: HASHPILOT_SOURCE_CHANNEL=<branch> skips npm and installs that exact"
279
+ echo " git branch instead (e.g. for bleeding-edge testing)."
280
+ echo " HASHPILOT_NPM_REGISTRY=<url> overrides the npm registry base URL."
281
+ echo ""
81
282
  echo "One-liner: curl -fsSL https://raw.githubusercontent.com/bigknoxy/HashPilot/main/scripts/install.sh | bash"
82
283
  exit 0
83
284
  ;;
@@ -186,9 +387,50 @@ detail "Core source copied to $TARGET_DIR/structured-editing"
186
387
 
187
388
  # ── Install dependencies ────────────────────────────────────────────────
188
389
  log "Installing dependencies..."
189
- cd "$TARGET_DIR/structured-editing"
190
- bun install --frozen-lockfile 2>&1 | while IFS= read -r line; do detail "$line"; done
191
- cd "$OLDPWD"
390
+ # Decide frozen-vs-production from $SOURCE_DIR (what we just copied FROM),
391
+ # not from whatever bun.lock might already be sitting in the target
392
+ # directory. The rsync fallback for hosts without rsync (`cp -r`, a few
393
+ # lines up) does not delete files absent from the source — an upgrade from
394
+ # a prior git-sourced install (which does ship bun.lock) to a new npm-
395
+ # sourced one (which doesn't) would otherwise leave the old lockfile
396
+ # behind, be found by a target-relative `[ -f bun.lock ]` check, and run
397
+ # --frozen-lockfile against the npm package's own package.json — which
398
+ # never matches, and hard-aborts the upgrade after node_modules has
399
+ # already been removed, leaving no working install at all.
400
+ #
401
+ # $NPM_INSTALLED (set above, always defined regardless of which branch was
402
+ # taken) is the authoritative signal for "this came from npm and has no
403
+ # lockfile" — cross-checked against bun.lock's presence rather than relied
404
+ # on alone, so a future source shape that disagrees with what we expect
405
+ # (e.g. an npm extraction that somehow shipped a lockfile, or a git/local
406
+ # source that's missing one) fails loudly here instead of silently
407
+ # guessing.
408
+ if [ "$NPM_INSTALLED" = "true" ] && [ -f "$SOURCE_DIR/bun.lock" ]; then
409
+ err "npm-sourced install unexpectedly has a bun.lock — refusing to guess which dependency mode is correct"
410
+ exit 1
411
+ fi
412
+ if [ "$NPM_INSTALLED" = "false" ] && [ ! -f "$SOURCE_DIR/bun.lock" ]; then
413
+ err "Source is missing bun.lock and wasn't installed from npm — refusing to guess which dependency mode is correct"
414
+ err "(local-clone and --source installs are expected to have bun.lock, same as the git repo does)"
415
+ exit 1
416
+ fi
417
+
418
+ if [ -f "$SOURCE_DIR/bun.lock" ]; then
419
+ cd "$TARGET_DIR/structured-editing"
420
+ bun install --frozen-lockfile 2>&1 | while IFS= read -r line; do detail "$line"; done
421
+ cd "$OLDPWD"
422
+ else
423
+ # The npm-published package.json still lists devDependencies (npm's
424
+ # `files` field controls which FILES ship, not which package.json fields
425
+ # do) — a plain `bun install` would resolve and install semantic-release,
426
+ # fast-check, and the rest of the dev toolchain for no reason on an end
427
+ # user's machine. --production skips them; the CLI never needs them.
428
+ detail "No bun.lock shipped (npm package install) — resolving production dependencies fresh"
429
+ rm -f "$TARGET_DIR/structured-editing/bun.lock"
430
+ cd "$TARGET_DIR/structured-editing"
431
+ bun install --production 2>&1 | while IFS= read -r line; do detail "$line"; done
432
+ cd "$OLDPWD"
433
+ fi
192
434
  detail "Dependencies installed"
193
435
 
194
436
  # ── Create CLI launcher ──────────────────────────────────────────────────
@@ -25,7 +25,7 @@ export function register(program: Command): void {
25
25
 
26
26
  program
27
27
  .command("upgrade")
28
- .description("Upgrade HashPilot to the latest version from GitHub")
28
+ .description("Upgrade HashPilot to the latest version (npm, falling back to GitHub)")
29
29
  .option("--channel <channel>", "Release channel (default: main)", "main")
30
30
  .option("--target <dir>", "Install target directory (default: ~/.agentic-tools)")
31
31
  .option("--keep-telemetry", "Preserve existing telemetry on upgrade")
@@ -55,8 +55,12 @@ export function register(program: Command): void {
55
55
  }
56
56
  const script = await response.text();
57
57
 
58
- // Write script to temp file and execute
58
+ // Write script to temp file and execute. targetDir may not exist yet
59
+ // on a genuinely first-time install (the `uninstall` command below
60
+ // already does this — `upgrade` didn't, and failed with a plain
61
+ // ENOENT on a brand-new target).
59
62
  const tmpScript = join(targetDir, `.hashpilot-upgrade-${Date.now()}.sh`);
63
+ mkdirSync(targetDir, { recursive: true });
60
64
  writeFileSync(tmpScript, script, { mode: 0o755 });
61
65
 
62
66
  const args = ["--target", targetDir];
@@ -66,7 +70,20 @@ export function register(program: Command): void {
66
70
  const proc = Bun.spawn(["bash", tmpScript, ...args], {
67
71
  stdout: "pipe",
68
72
  stderr: "pipe",
69
- env: { ...process.env, PATH: `${join(targetDir, "bin")}:${process.env.PATH || ""}` },
73
+ env: {
74
+ ...process.env,
75
+ PATH: `${join(targetDir, "bin")}:${process.env.PATH || ""}`,
76
+ // Always set explicitly (never omitted) so a stale
77
+ // HASHPILOT_SOURCE_CHANNEL already exported in the caller's
78
+ // shell or a CI job's environment can't silently override the
79
+ // channel actually requested on *this* invocation — install.sh
80
+ // skips its npm-primary source fetch entirely when this is set
81
+ // to anything other than "main", since npm's published releases
82
+ // can't provide an arbitrary git ref, so an inherited stale
83
+ // value would silently install from git instead of npm with no
84
+ // warning at all.
85
+ HASHPILOT_SOURCE_CHANNEL: channel === "main" ? "" : channel,
86
+ },
70
87
  });
71
88
 
72
89
  const stdout = await new Response(proc.stdout).text();