teleportation-cli 1.0.0 → 1.0.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/.claude/hooks/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/stop.mjs +205 -1
- package/.claude/hooks/user_prompt_submit.mjs +111 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/daemon/teleportation-daemon.js +131 -41
- package/lib/install/installer.js +22 -7
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
import { stdin, stdout, exit, env } from 'node:process';
|
|
10
10
|
import { tmpdir } from 'os';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
+
import { appendFile } from 'fs/promises';
|
|
13
|
+
|
|
14
|
+
const HOOK_LOG = '/tmp/teleportation-hook.log';
|
|
15
|
+
const log = async (msg) => {
|
|
16
|
+
const ts = new Date().toISOString();
|
|
17
|
+
try {
|
|
18
|
+
await appendFile(HOOK_LOG, `[${ts}] [UserPromptSubmit] ${msg}\n`);
|
|
19
|
+
} catch {}
|
|
20
|
+
};
|
|
12
21
|
|
|
13
22
|
const readStdin = () => new Promise((resolve, reject) => {
|
|
14
23
|
let data = '';
|
|
@@ -18,6 +27,12 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
18
27
|
stdin.on('error', reject);
|
|
19
28
|
});
|
|
20
29
|
|
|
30
|
+
const fetchJson = async (url, opts) => {
|
|
31
|
+
const res = await fetch(url, opts);
|
|
32
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
33
|
+
return res.json();
|
|
34
|
+
};
|
|
35
|
+
|
|
21
36
|
(async () => {
|
|
22
37
|
let input = {};
|
|
23
38
|
try {
|
|
@@ -31,6 +46,45 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
31
46
|
|
|
32
47
|
const { session_id, prompt } = input;
|
|
33
48
|
|
|
49
|
+
// Clear away mode only on actual user activity (prompt submit), not on tool attempts.
|
|
50
|
+
// Also support /away and /back here in case they are handled as prompts.
|
|
51
|
+
if (session_id && prompt && typeof prompt === 'string') {
|
|
52
|
+
const trimmed = prompt.trim();
|
|
53
|
+
const lowered = trimmed.toLowerCase();
|
|
54
|
+
|
|
55
|
+
let desiredAway = null;
|
|
56
|
+
if (lowered === '/away' || lowered === 'teleportation away') desiredAway = true;
|
|
57
|
+
else if (lowered === '/back' || lowered === 'teleportation back') desiredAway = false;
|
|
58
|
+
else if (trimmed.length > 0) desiredAway = false;
|
|
59
|
+
|
|
60
|
+
if (desiredAway !== null) {
|
|
61
|
+
try {
|
|
62
|
+
const { loadConfig } = await import('./config-loader.mjs');
|
|
63
|
+
const config = await loadConfig();
|
|
64
|
+
|
|
65
|
+
const RELAY_API_URL = env.RELAY_API_URL || config.relayApiUrl || '';
|
|
66
|
+
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
67
|
+
|
|
68
|
+
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
69
|
+
await fetchJson(`${RELAY_API_URL}/api/sessions/${session_id}/daemon-state`, {
|
|
70
|
+
method: 'PATCH',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ is_away: desiredAway })
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Always log failures to hook log file for debugging (not just in DEBUG mode)
|
|
80
|
+
log(`Failed to update away state: ${e.message}`);
|
|
81
|
+
if (env.DEBUG) {
|
|
82
|
+
console.error(`[UserPromptSubmit] Failed to update away state: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
34
88
|
// Check if user is running /model command
|
|
35
89
|
if (prompt && typeof prompt === 'string') {
|
|
36
90
|
const trimmed = prompt.trim().toLowerCase();
|
|
@@ -85,6 +139,63 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
85
139
|
}
|
|
86
140
|
}
|
|
87
141
|
|
|
142
|
+
// Log user message to timeline (skip special commands that are already logged differently)
|
|
143
|
+
if (session_id && prompt && typeof prompt === 'string') {
|
|
144
|
+
const trimmed = prompt.trim();
|
|
145
|
+
const lowered = trimmed.toLowerCase();
|
|
146
|
+
|
|
147
|
+
// Skip special commands - they are handled above or have their own logging
|
|
148
|
+
const isSpecialCommand =
|
|
149
|
+
lowered === '/away' || lowered === 'teleportation away' ||
|
|
150
|
+
lowered === '/back' || lowered === 'teleportation back' ||
|
|
151
|
+
lowered === '/model' || lowered.startsWith('/model ');
|
|
152
|
+
|
|
153
|
+
if (!isSpecialCommand && trimmed.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
const { loadConfig } = await import('./config-loader.mjs');
|
|
156
|
+
const config = await loadConfig();
|
|
157
|
+
const RELAY_API_URL = env.RELAY_API_URL || config.relayApiUrl || '';
|
|
158
|
+
const RELAY_API_KEY = env.RELAY_API_KEY || config.relayApiKey || '';
|
|
159
|
+
|
|
160
|
+
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
161
|
+
// Truncate prompt to 2000 chars for storage efficiency
|
|
162
|
+
const MAX_PROMPT_LENGTH = 2000;
|
|
163
|
+
const truncatedPrompt = trimmed.length > MAX_PROMPT_LENGTH
|
|
164
|
+
? trimmed.substring(0, MAX_PROMPT_LENGTH)
|
|
165
|
+
: trimmed;
|
|
166
|
+
|
|
167
|
+
await fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'Content-Type': 'application/json',
|
|
171
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
session_id,
|
|
175
|
+
type: 'user_message',
|
|
176
|
+
data: {
|
|
177
|
+
prompt: truncatedPrompt,
|
|
178
|
+
full_length: trimmed.length,
|
|
179
|
+
truncated: trimmed.length > MAX_PROMPT_LENGTH,
|
|
180
|
+
timestamp: Date.now()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (env.DEBUG) {
|
|
186
|
+
log(`Logged user_message to timeline for session ${session_id}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Non-critical - log error but don't block prompt submission
|
|
191
|
+
log(`Failed to log user message to timeline: ${e.message}`);
|
|
192
|
+
if (env.DEBUG) {
|
|
193
|
+
console.error(`[UserPromptSubmit] Failed to log user message: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
88
199
|
// Always suppress output from this hook
|
|
89
200
|
try { stdout.write(JSON.stringify({ suppressOutput: true })); } catch {}
|
|
90
201
|
return exit(0);
|
package/README.md
CHANGED
|
@@ -10,13 +10,29 @@ Teleportation enables developers to approve Claude Code actions remotely from an
|
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
-
### Method 1:
|
|
13
|
+
### Method 1: npm (Recommended)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install globally via npm
|
|
17
|
+
npm install -g teleportation-cli
|
|
18
|
+
|
|
19
|
+
# Verify installation
|
|
20
|
+
teleportation --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Requirements:**
|
|
24
|
+
- **Bun >=1.3.0** - [Install Bun](https://bun.sh/docs/installation)
|
|
25
|
+
- **Claude Code CLI** - [Install Claude Code](https://claude.ai/code)
|
|
26
|
+
|
|
27
|
+
> **Note:** While the package uses npm for distribution, it requires Bun as the runtime. Make sure to install Bun first.
|
|
28
|
+
|
|
29
|
+
### Method 2: Quick Install (curl)
|
|
14
30
|
|
|
15
31
|
```bash
|
|
16
32
|
curl -fsSL https://raw.githubusercontent.com/dundas/teleportation-private/main/scripts/install.sh | bash
|
|
17
33
|
```
|
|
18
34
|
|
|
19
|
-
### Method
|
|
35
|
+
### Method 3: From Source
|
|
20
36
|
|
|
21
37
|
**Requirements:** Bun >=1.3.0
|
|
22
38
|
|
|
@@ -31,15 +47,10 @@ bun install
|
|
|
31
47
|
bun link # Makes 'teleportation' command available globally
|
|
32
48
|
```
|
|
33
49
|
|
|
34
|
-
### Method
|
|
50
|
+
### Method 4: GitHub Releases
|
|
35
51
|
|
|
36
52
|
Download the latest release from [GitHub Releases](https://github.com/dundas/teleportation-private/releases/latest) and extract to `~/.teleportation/`.
|
|
37
53
|
|
|
38
|
-
## Requirements
|
|
39
|
-
|
|
40
|
-
- **Bun >=1.3.0** (JavaScript runtime)
|
|
41
|
-
- **Claude Code CLI** installed
|
|
42
|
-
|
|
43
54
|
### Installing Bun
|
|
44
55
|
|
|
45
56
|
```bash
|
|
@@ -77,16 +88,19 @@ bun install
|
|
|
77
88
|
## Quick Start
|
|
78
89
|
|
|
79
90
|
```bash
|
|
80
|
-
# 1.
|
|
91
|
+
# 1. Install via npm
|
|
92
|
+
npm install -g teleportation-cli
|
|
93
|
+
|
|
94
|
+
# 2. Enable hooks
|
|
81
95
|
teleportation on
|
|
82
96
|
|
|
83
|
-
#
|
|
97
|
+
# 3. Authenticate with your account
|
|
84
98
|
teleportation login
|
|
85
99
|
|
|
86
|
-
#
|
|
100
|
+
# 4. Check status
|
|
87
101
|
teleportation status
|
|
88
102
|
|
|
89
|
-
#
|
|
103
|
+
# 5. Start Claude Code - approvals will be routed to your phone!
|
|
90
104
|
```
|
|
91
105
|
|
|
92
106
|
## Usage
|
|
@@ -229,6 +243,16 @@ bun run dev:all
|
|
|
229
243
|
- **Multi-tenant isolation**: Your data is isolated from other users
|
|
230
244
|
- **Privacy-preserving**: Session existence not revealed to unauthorized users
|
|
231
245
|
|
|
246
|
+
## Known Limitations
|
|
247
|
+
|
|
248
|
+
### Session Registry Race Condition
|
|
249
|
+
|
|
250
|
+
The remote session registry uses file-based storage (`~/.teleportation/remote-sessions/sessions.json`). There is a theoretical race condition if multiple processes attempt to register the same session ID simultaneously. The check for existing sessions and the save operation are not atomic.
|
|
251
|
+
|
|
252
|
+
**Impact**: This is an acceptable limitation for the current single-user CLI use case.
|
|
253
|
+
|
|
254
|
+
**Future mitigation**: Future versions may implement file locking (e.g., using `proper-lockfile` package) or migrate to a database (e.g., SQLite) for multi-user or concurrent scenarios.
|
|
255
|
+
|
|
232
256
|
## License
|
|
233
257
|
|
|
234
258
|
MIT
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code API Key Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts the ANTHROPIC_API_KEY from the user's local Claude Code installation.
|
|
5
|
+
* Tries multiple methods to find the key.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Try to get API key from macOS keychain
|
|
18
|
+
*/
|
|
19
|
+
async function getFromKeychain() {
|
|
20
|
+
try {
|
|
21
|
+
// Try common keychain entries for Claude/Anthropic
|
|
22
|
+
const services = [
|
|
23
|
+
'claude.ai',
|
|
24
|
+
'anthropic.com',
|
|
25
|
+
'Claude',
|
|
26
|
+
'Anthropic',
|
|
27
|
+
'claude-code',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const service of services) {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execAsync(`security find-generic-password -s "${service}" -w 2>/dev/null`);
|
|
33
|
+
if (stdout && stdout.trim().startsWith('sk-ant-')) {
|
|
34
|
+
return stdout.trim();
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Try finding by account name
|
|
40
|
+
const accounts = ['api-key', 'apikey', 'token'];
|
|
41
|
+
for (const acct of accounts) {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await execAsync(`security find-generic-password -a "${acct}" -w 2>/dev/null`);
|
|
44
|
+
if (stdout && stdout.trim().startsWith('sk-ant-')) {
|
|
45
|
+
return stdout.trim();
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Try to get API key from environment
|
|
58
|
+
*/
|
|
59
|
+
function getFromEnvironment() {
|
|
60
|
+
return process.env.ANTHROPIC_API_KEY ||
|
|
61
|
+
process.env.CLAUDE_API_KEY ||
|
|
62
|
+
process.env.CLAUDE_CODE_API_KEY ||
|
|
63
|
+
null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Try to get API key from Claude Code config files
|
|
68
|
+
*/
|
|
69
|
+
async function getFromClaudeConfig() {
|
|
70
|
+
try {
|
|
71
|
+
const homeDir = os.homedir();
|
|
72
|
+
const configPaths = [
|
|
73
|
+
path.join(homeDir, '.claude', 'config.json'),
|
|
74
|
+
path.join(homeDir, '.claude', 'settings.json'),
|
|
75
|
+
path.join(homeDir, '.claude', 'credentials.json'),
|
|
76
|
+
path.join(homeDir, '.config', 'claude', 'config.json'),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const configPath of configPaths) {
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
82
|
+
const config = JSON.parse(content);
|
|
83
|
+
|
|
84
|
+
// Check various possible key names
|
|
85
|
+
const key = config.apiKey ||
|
|
86
|
+
config.anthropicApiKey ||
|
|
87
|
+
config.ANTHROPIC_API_KEY ||
|
|
88
|
+
config.api_key ||
|
|
89
|
+
config.token;
|
|
90
|
+
|
|
91
|
+
if (key && key.startsWith('sk-ant-')) {
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Try to get API key from running Claude Code process environment
|
|
105
|
+
*/
|
|
106
|
+
async function getFromRunningProcess() {
|
|
107
|
+
try {
|
|
108
|
+
// Get PID of running claude process
|
|
109
|
+
const { stdout: pids } = await execAsync(`pgrep -f "claude" 2>/dev/null || echo ""`);
|
|
110
|
+
|
|
111
|
+
if (!pids.trim()) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Try to read environment from process
|
|
116
|
+
for (const pid of pids.trim().split('\n')) {
|
|
117
|
+
if (!pid) continue;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// On macOS, we can try to read process environment via ps
|
|
121
|
+
const { stdout } = await execAsync(`ps eww ${pid} 2>/dev/null | grep -o "ANTHROPIC_API_KEY=[^ ]*" | cut -d= -f2`);
|
|
122
|
+
if (stdout && stdout.trim().startsWith('sk-ant-')) {
|
|
123
|
+
return stdout.trim();
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Main function to extract Claude Code API key
|
|
136
|
+
* Tries multiple methods in order of reliability
|
|
137
|
+
*
|
|
138
|
+
* @returns {Promise<string|null>} API key if found, null otherwise
|
|
139
|
+
*/
|
|
140
|
+
export async function extractClaudeApiKey() {
|
|
141
|
+
console.log('[claude-key-extractor] Attempting to extract API key from local Claude Code installation...');
|
|
142
|
+
|
|
143
|
+
// Method 1: Check environment first (fastest)
|
|
144
|
+
let apiKey = getFromEnvironment();
|
|
145
|
+
if (apiKey) {
|
|
146
|
+
console.log('[claude-key-extractor] ✅ Found API key in environment variables');
|
|
147
|
+
return apiKey;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Method 2: Check Claude Code config files
|
|
151
|
+
apiKey = await getFromClaudeConfig();
|
|
152
|
+
if (apiKey) {
|
|
153
|
+
console.log('[claude-key-extractor] ✅ Found API key in Claude Code config');
|
|
154
|
+
return apiKey;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Method 3: Check macOS keychain
|
|
158
|
+
apiKey = await getFromKeychain();
|
|
159
|
+
if (apiKey) {
|
|
160
|
+
console.log('[claude-key-extractor] ✅ Found API key in macOS keychain');
|
|
161
|
+
return apiKey;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Method 4: Try to read from running process
|
|
165
|
+
apiKey = await getFromRunningProcess();
|
|
166
|
+
if (apiKey) {
|
|
167
|
+
console.log('[claude-key-extractor] ✅ Found API key in running Claude Code process');
|
|
168
|
+
return apiKey;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log('[claude-key-extractor] ⚠️ Could not find API key');
|
|
172
|
+
console.log('[claude-key-extractor] Please set ANTHROPIC_API_KEY environment variable');
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get API key with fallback message
|
|
178
|
+
*/
|
|
179
|
+
export async function getClaudeApiKeyOrPrompt() {
|
|
180
|
+
const apiKey = await extractClaudeApiKey();
|
|
181
|
+
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
console.log('\n' + '='.repeat(60));
|
|
184
|
+
console.log('ANTHROPIC_API_KEY not found!');
|
|
185
|
+
console.log('='.repeat(60));
|
|
186
|
+
console.log('\nPlease set your API key using one of these methods:\n');
|
|
187
|
+
console.log('1. Environment variable:');
|
|
188
|
+
console.log(' export ANTHROPIC_API_KEY="sk-ant-..."');
|
|
189
|
+
console.log('\n2. Get your key from: https://console.anthropic.com/settings/keys');
|
|
190
|
+
console.log('\n3. If you\'re logged into Claude Code, try:');
|
|
191
|
+
console.log(' claude config show # (if supported)');
|
|
192
|
+
console.log('='.repeat(60) + '\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return apiKey;
|
|
196
|
+
}
|
package/lib/auth/credentials.js
CHANGED
|
@@ -27,8 +27,13 @@ async function getEncryptionKey(keyPath = DEFAULT_KEY_PATH) {
|
|
|
27
27
|
// For Windows: use Credential Manager or fallback to file
|
|
28
28
|
|
|
29
29
|
const platform = process.platform;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
// Only attempt keychain when using the default key path. This allows tests and
|
|
32
|
+
// advanced callers to provide a custom key path without depending on keychain
|
|
33
|
+
// availability/behavior.
|
|
34
|
+
const shouldUseKeychain = keyPath === DEFAULT_KEY_PATH;
|
|
35
|
+
|
|
36
|
+
if (shouldUseKeychain && platform === 'darwin') {
|
|
32
37
|
// macOS - try to use keychain
|
|
33
38
|
try {
|
|
34
39
|
const { execSync } = await import('child_process');
|