@dst-justin/relay 2.1.0 → 2.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.
Files changed (3) hide show
  1. package/package.json +3 -2
  2. package/postinstall.js +84 -0
  3. package/relay +114 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dst-justin/relay",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Multi-account switcher for Claude Code — instant credential swap across macOS, Linux, and Windows",
5
5
  "bin": {
6
6
  "relay": "./relay.js"
@@ -10,10 +10,11 @@
10
10
  "relay.js",
11
11
  "relay.ps1",
12
12
  "relay.cmd",
13
+ "postinstall.js",
13
14
  "README.md"
14
15
  ],
15
16
  "scripts": {
16
- "postinstall": "node -e \"if(process.platform!=='win32'){try{require('fs').chmodSync(require('path').join(__dirname,'relay'),'755')}catch(_){}}\""
17
+ "postinstall": "node -e \"if(process.platform!=='win32'){try{require('fs').chmodSync(require('path').join(__dirname,'relay'),'755')}catch(_){}}\" && node postinstall.js"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">=16"
package/postinstall.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // Runs after `npm install -g @dst-justin/relay`.
3
+ // If the npm global bin dir is not in PATH, appends it to shell config files.
4
+ 'use strict';
5
+
6
+ const { execSync } = require('child_process');
7
+ const { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync } = require('fs');
8
+ const { join } = require('path');
9
+ const os = require('os');
10
+
11
+ // ── Windows ───────────────────────────────────────────────────────────────────
12
+ // npm on Windows already manages PATH via the installer; nothing to do here.
13
+ if (process.platform === 'win32') {
14
+ // Verify relay.js can reach relay.ps1 (sanity check)
15
+ if (!existsSync(join(__dirname, 'relay.ps1'))) {
16
+ console.warn(' relay: warning — relay.ps1 not found, Windows support may be broken');
17
+ }
18
+ process.exit(0);
19
+ }
20
+
21
+ // ── Get npm global bin dir ────────────────────────────────────────────────────
22
+ // npm sets npm_config_prefix during install; fall back to `npm prefix -g`
23
+ let prefix = process.env.npm_config_prefix;
24
+ if (!prefix) {
25
+ try {
26
+ prefix = execSync('npm prefix -g', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
27
+ } catch (_) {
28
+ process.exit(0); // can't determine, give up silently
29
+ }
30
+ }
31
+ const binDir = join(prefix, 'bin');
32
+
33
+ // Already in PATH — nothing to do
34
+ const pathDirs = (process.env.PATH || '').split(':');
35
+ if (pathDirs.includes(binDir)) process.exit(0);
36
+
37
+ const home = os.homedir();
38
+
39
+ // ── POSIX shells: bash / zsh / sh ────────────────────────────────────────────
40
+ const exportLine = `export PATH="${binDir}:$PATH"`;
41
+ const marker = '# added by relay';
42
+ const block = `\n${exportLine} ${marker}\n`;
43
+
44
+ const rcFiles = ['.bashrc', '.zshrc', '.profile'].map(f => join(home, f));
45
+ let patched = false;
46
+
47
+ for (const rc of rcFiles) {
48
+ if (!existsSync(rc)) continue;
49
+ try {
50
+ if (readFileSync(rc, 'utf8').includes(marker)) continue; // idempotent
51
+ appendFileSync(rc, block);
52
+ console.log(` relay: added ${binDir} to PATH in ~/${require('path').basename(rc)}`);
53
+ patched = true;
54
+ } catch (_) {}
55
+ }
56
+
57
+ // If no shell rc found, create ~/.bashrc as a last resort
58
+ if (!patched) {
59
+ const bashrc = join(home, '.bashrc');
60
+ try {
61
+ appendFileSync(bashrc, block);
62
+ console.log(` relay: created ~/.bashrc with PATH entry for ${binDir}`);
63
+ patched = true;
64
+ } catch (_) {}
65
+ }
66
+
67
+ // ── Fish shell ────────────────────────────────────────────────────────────────
68
+ const fishConfig = join(home, '.config', 'fish', 'config.fish');
69
+ const fishMarker = '# added by relay';
70
+ const fishLine = `\nset -gx PATH "${binDir}" $PATH ${fishMarker}\n`;
71
+
72
+ if (existsSync(fishConfig)) {
73
+ try {
74
+ if (!readFileSync(fishConfig, 'utf8').includes(fishMarker)) {
75
+ appendFileSync(fishConfig, fishLine);
76
+ console.log(` relay: added ${binDir} to PATH in ~/.config/fish/config.fish`);
77
+ patched = true;
78
+ }
79
+ } catch (_) {}
80
+ }
81
+
82
+ if (patched) {
83
+ console.log(` relay: restart your shell (or 'source ~/.bashrc' / 'source ~/.zshrc') then try: relay list`);
84
+ }
package/relay CHANGED
@@ -12,6 +12,7 @@ RELAY_DIR="${HOME}/.claude-relay"
12
12
  CREDS_STORE="${RELAY_DIR}/credentials"
13
13
  META_STORE="${RELAY_DIR}/meta"
14
14
  CURRENT_FILE="${RELAY_DIR}/current"
15
+ UPDATE_CACHE="${RELAY_DIR}/.update_cache"
15
16
  CLAUDE_DIR="${HOME}/.claude"
16
17
  CLAUDE_JSON="${HOME}/.claude.json"
17
18
  REAL_CLAUDE=$(command -v claude 2>/dev/null || echo "")
@@ -371,14 +372,21 @@ _sync_current_creds() {
371
372
  [[ -n "${live}" ]] && printf '%s' "${live}" > "$(account_creds "${cur}")"
372
373
  }
373
374
 
374
- cmd_quick() { _sync_current_creds; render_table quick "${CREDS_STORE}" "${META_STORE}" "$(current_name)" "$@"; }
375
+ cmd_quick() {
376
+ _check_update_bg
377
+ _sync_current_creds
378
+ render_table quick "${CREDS_STORE}" "${META_STORE}" "$(current_name)" "$@"
379
+ _show_update_notice
380
+ }
375
381
 
376
382
  cmd_list() {
383
+ _check_update_bg
377
384
  hdr "Account List"
378
385
  _sync_current_creds
379
386
  render_table full "${CREDS_STORE}" "${META_STORE}" "$(current_name)" "$@"
380
387
  echo ""
381
388
  ok "Inside Claude Code: ${CY}!relay <index>${R} to switch"
389
+ _show_update_notice
382
390
  }
383
391
 
384
392
  cmd_status() {
@@ -450,6 +458,8 @@ EOF
450
458
  [[ -d "${CLAUDE_DIR}/projects" ]] && \
451
459
  n=$(find "${CLAUDE_DIR}/projects" -name "*.jsonl" 2>/dev/null | wc -l | tr -d ' ')
452
460
  printf "\n ${B}Sessions:${R} %s (shared across all accounts in ~/.claude/projects/)\n" "${n}"
461
+ _check_update_bg
462
+ _show_update_notice
453
463
  }
454
464
 
455
465
  cmd_add() {
@@ -599,6 +609,8 @@ print()
599
609
  print(f' {total} session(s)' if total else ' No sessions found')
600
610
  EOF
601
611
  log "${CY}claude -c${R} resume last ${D}|${R} ${CY}claude -r${R} pick one ${D}|${R} ${CY}claude --resume <id>${R}"
612
+ _check_update_bg
613
+ _show_update_notice
602
614
  }
603
615
 
604
616
  cmd_install() {
@@ -1190,6 +1202,68 @@ cmd_version() {
1190
1202
  printf "relay %s\n" "$(_read_version)"
1191
1203
  }
1192
1204
 
1205
+ # ── Update notification helpers ───────────────────────────────────────────────
1206
+ # Cache format: "<epoch>:<version>" TTL = 24h
1207
+ _check_update_bg() {
1208
+ (
1209
+ local ttl=86400
1210
+ if [[ -f "${UPDATE_CACHE}" ]]; then
1211
+ local cached; cached=$(cat "${UPDATE_CACHE}" 2>/dev/null)
1212
+ local ts="${cached%%:*}"
1213
+ local now; now=$(date +%s)
1214
+ [[ $(( now - ts )) -lt ${ttl} ]] && exit 0
1215
+ fi
1216
+ local ver
1217
+ ver=$("${PY}" - 2>/dev/null <<'PYEOF'
1218
+ import urllib.request, json, sys
1219
+ def fetch(url, h={}):
1220
+ r = urllib.request.Request(url, headers=h)
1221
+ with urllib.request.urlopen(r, timeout=6) as resp:
1222
+ return json.loads(resp.read())
1223
+ try:
1224
+ d = fetch('https://api.github.com/repos/darkstar1227/relay/releases/latest',
1225
+ {'User-Agent': 'relay-update'})
1226
+ print(d['tag_name'].lstrip('v')); sys.exit(0)
1227
+ except Exception: pass
1228
+ try:
1229
+ d = fetch('https://registry.npmjs.org/@dst-justin%2frelay/latest')
1230
+ print(d['version'])
1231
+ except Exception: sys.exit(1)
1232
+ PYEOF
1233
+ )
1234
+ [[ -n "${ver}" ]] && printf '%s:%s' "$(date +%s)" "${ver}" > "${UPDATE_CACHE}"
1235
+ ) >/dev/null 2>&1 &
1236
+ disown 2>/dev/null || true
1237
+ }
1238
+
1239
+ _show_update_notice() {
1240
+ local current; current=$(_read_version)
1241
+ local latest=""
1242
+ if [[ -f "${UPDATE_CACHE}" ]]; then
1243
+ local cached; cached=$(cat "${UPDATE_CACHE}" 2>/dev/null)
1244
+ latest="${cached#*:}"
1245
+ fi
1246
+ if [[ -n "${latest}" && "${latest}" != "${current}" ]]; then
1247
+ printf "\n ${D}relay version: ${B}${current}${R}${D} → ${CY}${B}${latest}${R}${D} available — run ${CY}relay update${R}${D} to install${R}\n"
1248
+ else
1249
+ local ver_display="${latest:-${current}}"
1250
+ printf "\n ${D}relay version: ${B}${ver_display}${R}${D} (up to date)${R}\n"
1251
+ fi
1252
+ }
1253
+
1254
+ # Detect how relay was originally installed:
1255
+ # npm — package.json present in script dir (npm unpacks the full package)
1256
+ # git — .git dir present in script dir
1257
+ # direct — bare script copy (no package.json, no .git)
1258
+ _detect_install_method() {
1259
+ local d; d=$(_script_dir)
1260
+ # .git check first: git clone has both .git AND package.json; npm publish strips .git
1261
+ if [[ -d "${d}/.git" ]]; then echo "git"
1262
+ elif [[ -f "${d}/package.json" ]]; then echo "npm"
1263
+ else echo "direct"
1264
+ fi
1265
+ }
1266
+
1193
1267
  cmd_update() {
1194
1268
  hdr "Update relay"
1195
1269
 
@@ -1229,18 +1303,43 @@ PYEOF
1229
1303
  fi
1230
1304
 
1231
1305
  local relay_dir; relay_dir=$(_script_dir)
1232
- local npm_cmd; npm_cmd=$(command -v npm 2>/dev/null)
1233
- if [[ -n "${npm_cmd}" ]]; then
1234
- log "Installing via npm..."
1235
- npm install -g @dst-justin/relay@latest
1236
- ok "Updated to $(_read_version)"
1237
- elif [[ -d "${relay_dir}/.git" ]]; then
1238
- log "npm not found updating via git..."
1239
- git -C "${relay_dir}" pull
1240
- else
1241
- err "Cannot update: npm not found and no .git directory"
1242
- log "Install via npm: ${CY}npm install -g @dst-justin/relay@latest${R}"
1243
- fi
1306
+ local method; method=$(_detect_install_method)
1307
+
1308
+ case "${method}" in
1309
+ npm)
1310
+ local npm_cmd; npm_cmd=$(command -v npm 2>/dev/null)
1311
+ if [[ -n "${npm_cmd}" ]]; then
1312
+ log "Updating via npm (original install method)..."
1313
+ npm install -g @dst-justin/relay@latest
1314
+ ok "Updated to $(_read_version)"
1315
+ else
1316
+ err "npm not found — reinstall npm and retry"
1317
+ log "Or update manually: ${CY}npm install -g @dst-justin/relay@latest${R}"
1318
+ fi ;;
1319
+ git)
1320
+ log "Updating via git pull (original install method)..."
1321
+ git -C "${relay_dir}" pull ;;
1322
+ direct)
1323
+ log "Updating via direct download (original install method)..."
1324
+ local script_path; script_path=$(readlink -f "$0" 2>/dev/null || echo "$0")
1325
+ "${PY}" - "${script_path}" "${latest}" <<'PYEOF'
1326
+ import urllib.request, sys, os, stat
1327
+ script_path, version = sys.argv[1], sys.argv[2]
1328
+ url = f'https://raw.githubusercontent.com/darkstar1227/relay/v{version}/relay'
1329
+ try:
1330
+ with urllib.request.urlopen(url, timeout=15) as r:
1331
+ content = r.read()
1332
+ tmp = script_path + '.tmp'
1333
+ with open(tmp, 'wb') as f: f.write(content)
1334
+ os.chmod(tmp, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
1335
+ os.replace(tmp, script_path)
1336
+ except Exception as e:
1337
+ print(f' download failed: {e}', file=sys.stderr); sys.exit(1)
1338
+ PYEOF
1339
+ ok "Updated ${script_path} to ${latest}" ;;
1340
+ esac
1341
+ # Invalidate update cache so next display shows fresh state
1342
+ rm -f "${UPDATE_CACHE}" 2>/dev/null || true
1244
1343
  }
1245
1344
 
1246
1345
  cmd_uninstall() {
@@ -1297,6 +1396,8 @@ cmd_help() {
1297
1396
  echo ""
1298
1397
  printf " ${D}switches the OAuth credential (macOS Keychain / Linux ~/.claude/.credentials.json); sessions are shared${R}\n"
1299
1398
  printf " ${D}after switching: claude -c to resume, claude --resume <id> for a specific session${R}\n\n"
1399
+ _check_update_bg
1400
+ _show_update_notice
1300
1401
  }
1301
1402
 
1302
1403
  # ══════════════════════════════════════════════════════════════════