life-pulse 2.4.1 → 2.4.2

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 (2) hide show
  1. package/dist/cli.js +88 -12
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ import { findHandlesForName } from './contacts.js';
28
28
  import { ensurePromptLayerFiles } from './prompt-layers.js';
29
29
  import { hasTailscale, startFunnel, getHostname as getTailscaleHostname } from './tunnel.js';
30
30
  import chalk from 'chalk';
31
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
31
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, cpSync, rmSync } from 'fs';
32
32
  import { join, dirname } from 'path';
33
33
  import { fileURLToPath } from 'url';
34
34
  import { homedir } from 'os';
@@ -96,6 +96,31 @@ function normalizeHostOrIp(raw) {
96
96
  t = t.replace(/:\d+$/, '');
97
97
  return t.trim();
98
98
  }
99
+ function resolveTailscaleBin() {
100
+ try {
101
+ const out = execSync('command -v tailscale', {
102
+ stdio: 'pipe',
103
+ timeout: 3000,
104
+ encoding: 'utf-8',
105
+ shell: '/bin/zsh',
106
+ }).trim();
107
+ if (out)
108
+ return out;
109
+ }
110
+ catch { }
111
+ const candidates = [
112
+ '/Applications/Tailscale.app/Contents/MacOS/Tailscale',
113
+ join(homedir(), 'Applications', 'Tailscale.app', 'Contents', 'MacOS', 'Tailscale'),
114
+ ];
115
+ for (const c of candidates) {
116
+ if (existsSync(c))
117
+ return c;
118
+ }
119
+ return '';
120
+ }
121
+ function hasUsableTailscale() {
122
+ return !!resolveTailscaleBin();
123
+ }
99
124
  function extractExecOutput(error) {
100
125
  const e = error;
101
126
  const stdout = typeof e?.stdout === 'string' ? e.stdout : Buffer.isBuffer(e?.stdout) ? e.stdout.toString('utf-8') : '';
@@ -137,11 +162,27 @@ function detectOpenClawToken() {
137
162
  return '';
138
163
  }
139
164
  function detectTailscaleHostOrIp() {
140
- const dns = getTailscaleHostname();
141
- if (dns)
142
- return normalizeHostOrIp(dns);
165
+ const bin = resolveTailscaleBin();
166
+ if (!bin)
167
+ return '';
168
+ try {
169
+ const out = execFileSync(bin, ['status', '--json'], {
170
+ stdio: 'pipe',
171
+ timeout: 5000,
172
+ encoding: 'utf-8',
173
+ });
174
+ const status = JSON.parse(out);
175
+ const dns = normalizeHostOrIp(String(status?.Self?.DNSName || '').replace(/\.$/, ''));
176
+ if (dns)
177
+ return dns;
178
+ const ips = Array.isArray(status?.Self?.TailscaleIPs) ? status.Self.TailscaleIPs : [];
179
+ const ip = ips.find((x) => typeof x === 'string' && /^100\.\d+\.\d+\.\d+$/.test(x));
180
+ if (ip)
181
+ return String(ip);
182
+ }
183
+ catch { }
143
184
  try {
144
- const out = execSync('tailscale ip -4', {
185
+ const out = execFileSync(bin, ['ip', '-4'], {
145
186
  stdio: 'pipe',
146
187
  timeout: 5000,
147
188
  encoding: 'utf-8',
@@ -166,32 +207,67 @@ function hasHomebrew() {
166
207
  }
167
208
  }
168
209
  function installTailscaleIfMissing() {
169
- if (hasTailscale())
210
+ if (hasUsableTailscale())
170
211
  return true;
171
- if (!hasHomebrew())
172
- return false;
212
+ if (hasHomebrew()) {
213
+ try {
214
+ execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
215
+ stdio: 'pipe',
216
+ timeout: 8 * 60 * 1000,
217
+ shell: '/bin/zsh',
218
+ });
219
+ }
220
+ catch { }
221
+ if (hasUsableTailscale())
222
+ return true;
223
+ }
224
+ // Homebrew-less fallback: install official app bundle from Tailscale zip.
173
225
  try {
174
- execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
226
+ const tmpDir = join('/tmp', `life-pulse-ts-${Date.now()}`);
227
+ mkdirSync(tmpDir, { recursive: true });
228
+ const zipPath = join(tmpDir, 'tailscale.zip');
229
+ execSync(`curl -L --fail -o ${JSON.stringify(zipPath)} https://pkgs.tailscale.com/stable/Tailscale-latest-macos.zip`, {
175
230
  stdio: 'pipe',
176
- timeout: 8 * 60 * 1000,
231
+ timeout: 3 * 60 * 1000,
177
232
  shell: '/bin/zsh',
178
233
  });
234
+ execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(tmpDir)}`, {
235
+ stdio: 'pipe',
236
+ timeout: 60_000,
237
+ shell: '/bin/zsh',
238
+ });
239
+ const appSrc = join(tmpDir, 'Tailscale.app');
240
+ if (existsSync(appSrc)) {
241
+ const appDestSystem = '/Applications/Tailscale.app';
242
+ const appDestUser = join(homedir(), 'Applications', 'Tailscale.app');
243
+ try {
244
+ cpSync(appSrc, appDestSystem, { recursive: true, force: true });
245
+ }
246
+ catch {
247
+ mkdirSync(join(homedir(), 'Applications'), { recursive: true });
248
+ cpSync(appSrc, appDestUser, { recursive: true, force: true });
249
+ }
250
+ }
251
+ rmSync(tmpDir, { recursive: true, force: true });
179
252
  }
180
253
  catch { }
181
- return hasTailscale();
254
+ return hasUsableTailscale();
182
255
  }
183
256
  function extractTailscaleAuthUrl(output) {
184
257
  const m = output.match(/https:\/\/login\.tailscale\.com\/[^\s"']+/i);
185
258
  return m ? m[0] : '';
186
259
  }
187
260
  function kickOffTailscaleAuth() {
261
+ const bin = resolveTailscaleBin();
262
+ if (!bin)
263
+ return;
188
264
  try {
189
265
  execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
190
266
  }
191
267
  catch { }
192
268
  let output = '';
193
269
  try {
194
- output = execSync('tailscale up --accept-dns=false', {
270
+ output = execFileSync(bin, ['up', '--accept-dns=false'], {
195
271
  stdio: 'pipe',
196
272
  timeout: 15_000,
197
273
  encoding: 'utf-8',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {