life-pulse 2.4.0 → 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.
- package/dist/cli.js +204 -4
- package/dist/ui/theme.js +17 -17
- 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';
|
|
@@ -87,6 +87,47 @@ function normalizePhoneCandidate(raw) {
|
|
|
87
87
|
return '+' + t.slice(1).replace(/\D/g, '');
|
|
88
88
|
return t.replace(/\D/g, '');
|
|
89
89
|
}
|
|
90
|
+
function normalizeHostOrIp(raw) {
|
|
91
|
+
let t = raw.trim();
|
|
92
|
+
if (!t)
|
|
93
|
+
return '';
|
|
94
|
+
t = t.replace(/^https?:\/\//i, '');
|
|
95
|
+
t = t.split('/')[0] || '';
|
|
96
|
+
t = t.replace(/:\d+$/, '');
|
|
97
|
+
return t.trim();
|
|
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
|
+
}
|
|
124
|
+
function extractExecOutput(error) {
|
|
125
|
+
const e = error;
|
|
126
|
+
const stdout = typeof e?.stdout === 'string' ? e.stdout : Buffer.isBuffer(e?.stdout) ? e.stdout.toString('utf-8') : '';
|
|
127
|
+
const stderr = typeof e?.stderr === 'string' ? e.stderr : Buffer.isBuffer(e?.stderr) ? e.stderr.toString('utf-8') : '';
|
|
128
|
+
const message = typeof e?.message === 'string' ? e.message : '';
|
|
129
|
+
return [stdout, stderr, message].filter(Boolean).join('\n');
|
|
130
|
+
}
|
|
90
131
|
async function promptLine(question, fallback = '') {
|
|
91
132
|
if (!process.stdin.isTTY)
|
|
92
133
|
return fallback;
|
|
@@ -120,6 +161,163 @@ function detectOpenClawToken() {
|
|
|
120
161
|
}
|
|
121
162
|
return '';
|
|
122
163
|
}
|
|
164
|
+
function detectTailscaleHostOrIp() {
|
|
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 { }
|
|
184
|
+
try {
|
|
185
|
+
const out = execFileSync(bin, ['ip', '-4'], {
|
|
186
|
+
stdio: 'pipe',
|
|
187
|
+
timeout: 5000,
|
|
188
|
+
encoding: 'utf-8',
|
|
189
|
+
});
|
|
190
|
+
const ip = out
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map(s => s.trim())
|
|
193
|
+
.find(s => /^100\.\d+\.\d+\.\d+$/.test(s));
|
|
194
|
+
if (ip)
|
|
195
|
+
return ip;
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
function hasHomebrew() {
|
|
201
|
+
try {
|
|
202
|
+
execSync('command -v brew', { stdio: 'pipe', timeout: 3000, encoding: 'utf-8' });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function installTailscaleIfMissing() {
|
|
210
|
+
if (hasUsableTailscale())
|
|
211
|
+
return true;
|
|
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.
|
|
225
|
+
try {
|
|
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`, {
|
|
230
|
+
stdio: 'pipe',
|
|
231
|
+
timeout: 3 * 60 * 1000,
|
|
232
|
+
shell: '/bin/zsh',
|
|
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 });
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
return hasUsableTailscale();
|
|
255
|
+
}
|
|
256
|
+
function extractTailscaleAuthUrl(output) {
|
|
257
|
+
const m = output.match(/https:\/\/login\.tailscale\.com\/[^\s"']+/i);
|
|
258
|
+
return m ? m[0] : '';
|
|
259
|
+
}
|
|
260
|
+
function kickOffTailscaleAuth() {
|
|
261
|
+
const bin = resolveTailscaleBin();
|
|
262
|
+
if (!bin)
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
let output = '';
|
|
269
|
+
try {
|
|
270
|
+
output = execFileSync(bin, ['up', '--accept-dns=false'], {
|
|
271
|
+
stdio: 'pipe',
|
|
272
|
+
timeout: 15_000,
|
|
273
|
+
encoding: 'utf-8',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
output = extractExecOutput(err);
|
|
278
|
+
}
|
|
279
|
+
const authUrl = extractTailscaleAuthUrl(output);
|
|
280
|
+
if (authUrl) {
|
|
281
|
+
try {
|
|
282
|
+
execSync(`open ${JSON.stringify(authUrl)}`, { stdio: 'pipe', timeout: 5000 });
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function waitForTailscaleHost(maxMs = 60_000) {
|
|
288
|
+
const start = Date.now();
|
|
289
|
+
while (Date.now() - start < maxMs) {
|
|
290
|
+
const host = detectTailscaleHostOrIp();
|
|
291
|
+
if (host)
|
|
292
|
+
return host;
|
|
293
|
+
await sleep(1500);
|
|
294
|
+
}
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
async function ensureTailscaleHostForPair(spinner) {
|
|
298
|
+
let host = detectTailscaleHostOrIp();
|
|
299
|
+
if (host)
|
|
300
|
+
return host;
|
|
301
|
+
spinner?.start('setting up secure link');
|
|
302
|
+
const installed = installTailscaleIfMissing();
|
|
303
|
+
if (!installed) {
|
|
304
|
+
spinner?.stop();
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
host = await waitForTailscaleHost(12_000);
|
|
312
|
+
if (host) {
|
|
313
|
+
spinner?.stop();
|
|
314
|
+
return host;
|
|
315
|
+
}
|
|
316
|
+
kickOffTailscaleAuth();
|
|
317
|
+
host = await waitForTailscaleHost(60_000);
|
|
318
|
+
spinner?.stop();
|
|
319
|
+
return host;
|
|
320
|
+
}
|
|
123
321
|
function writeNoxRouteJson(payload) {
|
|
124
322
|
const desktop = join(homedir(), 'Desktop');
|
|
125
323
|
mkdirSync(desktop, { recursive: true });
|
|
@@ -268,12 +466,13 @@ async function main() {
|
|
|
268
466
|
// --pair: generate Desktop/nox-route.json for NOX routing
|
|
269
467
|
if (pairMode) {
|
|
270
468
|
const defaultName = getUserName() || '';
|
|
271
|
-
const detectedHost = getTailscaleHostname() || '';
|
|
272
469
|
const detectedToken = detectOpenClawToken();
|
|
273
470
|
const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
|
|
274
471
|
|| process.env.NOX_OWNER_PHONE
|
|
275
472
|
|| process.env.LIFE_PULSE_BRIEF_SMS_PHONE
|
|
276
473
|
|| '');
|
|
474
|
+
const pairSpinner = process.stdin.isTTY ? new Spinner() : undefined;
|
|
475
|
+
const detectedHost = normalizeHostOrIp(await ensureTailscaleHostForPair(pairSpinner));
|
|
277
476
|
console.log();
|
|
278
477
|
console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
|
|
279
478
|
console.log(chalk.dim(' we will create Desktop/nox-route.json'));
|
|
@@ -285,9 +484,10 @@ async function main() {
|
|
|
285
484
|
console.log(chalk.red(' missing phone number'));
|
|
286
485
|
return;
|
|
287
486
|
}
|
|
288
|
-
const host =
|
|
487
|
+
const host = detectedHost;
|
|
289
488
|
if (!host) {
|
|
290
|
-
console.log(chalk.red('
|
|
489
|
+
console.log(chalk.red(' secure link not ready yet'));
|
|
490
|
+
console.log(chalk.dim(' keep tailscale signed in, then rerun: npx life-pulse --pair'));
|
|
291
491
|
return;
|
|
292
492
|
}
|
|
293
493
|
const token = await promptLine('openclaw token', detectedToken);
|
package/dist/ui/theme.js
CHANGED
|
@@ -7,23 +7,23 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
// ─── Palette ──────────────────────────────────────
|
|
9
9
|
export const C = {
|
|
10
|
-
// Brand —
|
|
11
|
-
accent: chalk.hex('#
|
|
12
|
-
brand: chalk.bold.hex('#
|
|
13
|
-
// Semantic — purple
|
|
14
|
-
ok: chalk.hex('#
|
|
15
|
-
warn: chalk.hex('#
|
|
16
|
-
err: chalk.bold.hex('#
|
|
17
|
-
info: chalk.hex('#
|
|
18
|
-
// Text hierarchy (
|
|
19
|
-
hd: chalk.bold.hex('#
|
|
20
|
-
heading: chalk.bold.hex('#
|
|
21
|
-
bright: chalk.hex('#
|
|
22
|
-
text: chalk.hex('#
|
|
23
|
-
mid: chalk.hex('#
|
|
24
|
-
dim: chalk.hex('#
|
|
25
|
-
faint: chalk.hex('#
|
|
26
|
-
rule: chalk.hex('#
|
|
10
|
+
// Brand — dark purple accent
|
|
11
|
+
accent: chalk.hex('#6b21a8'),
|
|
12
|
+
brand: chalk.bold.hex('#6b21a8'),
|
|
13
|
+
// Semantic — dark purple tones
|
|
14
|
+
ok: chalk.hex('#4a1d6e'),
|
|
15
|
+
warn: chalk.hex('#6b3fa0'),
|
|
16
|
+
err: chalk.bold.hex('#4a1d6e'),
|
|
17
|
+
info: chalk.hex('#6b3fa0'),
|
|
18
|
+
// Text hierarchy (darkest → lightest)
|
|
19
|
+
hd: chalk.bold.hex('#3b0764'),
|
|
20
|
+
heading: chalk.bold.hex('#3b0764'),
|
|
21
|
+
bright: chalk.hex('#4a1d6e'),
|
|
22
|
+
text: chalk.hex('#581c87'),
|
|
23
|
+
mid: chalk.hex('#7e22ce'),
|
|
24
|
+
dim: chalk.hex('#9333ea'),
|
|
25
|
+
faint: chalk.hex('#a855f7'),
|
|
26
|
+
rule: chalk.hex('#c084fc'),
|
|
27
27
|
};
|
|
28
28
|
// Lowercase alias (backward compat)
|
|
29
29
|
export const c = C;
|