trooper-cli 0.1.5 → 0.1.7
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 +14 -3
- package/bin/trooper.js +165 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official command-line installer for Trooper's local desktop runtime.
|
|
4
4
|
|
|
5
|
-
Trooper can run on your own Mac without a hosted cloud computer. This CLI installs the local bridge, starts the local AI gateway,
|
|
5
|
+
Trooper can run on your own Mac without a hosted cloud computer. This CLI installs the local bridge, starts the local AI gateway, installs the Trooper desktop app when it is missing, and opens Trooper so the machine can pair to your workspace.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ Install Trooper locally on this Mac:
|
|
|
12
12
|
npx -y trooper-cli
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The command opens the Trooper desktop app when setup finishes.
|
|
16
16
|
|
|
17
17
|
## What It Sets Up
|
|
18
18
|
|
|
@@ -23,9 +23,12 @@ On macOS, `npx -y trooper-cli` prepares:
|
|
|
23
23
|
- LaunchAgents so the local host can keep running
|
|
24
24
|
- A Docker-compatible runtime using existing Docker or Colima when needed
|
|
25
25
|
- Local runtime files under `~/Library/Application Support/Trooper/runtime`
|
|
26
|
+
- Trooper.app in `~/Applications` when the desktop app is not already installed
|
|
26
27
|
|
|
27
28
|
The local desktop plan does not require a Trooper cloud computer or Stripe checkout.
|
|
28
29
|
|
|
30
|
+
One computer can host one local Trooper workspace at a time. Re-running the CLI for the same workspace is safe. If the computer is already paired to another workspace, Trooper leaves that installation unchanged and asks you to use another computer, choose Trooper Cloud, or uninstall the existing local host first.
|
|
31
|
+
|
|
29
32
|
## Commands
|
|
30
33
|
|
|
31
34
|
### Install local runtime
|
|
@@ -40,6 +43,12 @@ This is shorthand for:
|
|
|
40
43
|
npx -y trooper-cli onboard --yes
|
|
41
44
|
```
|
|
42
45
|
|
|
46
|
+
Install only the runtime and skip desktop app install/open:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx -y trooper-cli --skip-app
|
|
50
|
+
```
|
|
51
|
+
|
|
43
52
|
### Install with a Trooper setup token
|
|
44
53
|
|
|
45
54
|
The Trooper app may generate a short-lived setup token for workspace pairing:
|
|
@@ -65,6 +74,8 @@ npx -y --prefer-online trooper-cli@latest uninstall --yes
|
|
|
65
74
|
|
|
66
75
|
This removes Trooper's local runtime, LaunchAgents, installer command file, and Trooper local Docker container. It leaves Docker, Colima, Homebrew, Node, and npm installed because those may be used by other projects.
|
|
67
76
|
|
|
77
|
+
When online, uninstall also releases this computer's workspace binding so it can be paired elsewhere.
|
|
78
|
+
|
|
68
79
|
Keep local workspace/config data:
|
|
69
80
|
|
|
70
81
|
```bash
|
|
@@ -85,7 +96,7 @@ npx -y --prefer-online trooper-cli@latest uninstall --yes --remove-app
|
|
|
85
96
|
|
|
86
97
|
## Desktop App
|
|
87
98
|
|
|
88
|
-
|
|
99
|
+
The CLI installs and opens the Trooper Mac app automatically when it is missing. If the app is already installed in `~/Applications`, `/Applications`, or registered with Launch Services, the CLI opens the existing app instead of downloading it again.
|
|
89
100
|
|
|
90
101
|
## Links
|
|
91
102
|
|
package/bin/trooper.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
5
8
|
|
|
6
9
|
const DEFAULT_API_URL = 'https://trooper-production.up.railway.app';
|
|
7
10
|
const LOCAL_MAC_SETUP_SCRIPT_URL = 'https://raw.githubusercontent.com/absurdfounder/trooper-bridge/main/setup-local-mac-host.sh';
|
|
11
|
+
const TROOPER_MAC_APP_DMG_URL = 'https://github.com/absurdfounder/trooper_landing/releases/download/macos-latest/Trooper.dmg';
|
|
8
12
|
const LOCAL_MAC_LABELS = [
|
|
9
13
|
'so.trooper.local-gateway',
|
|
10
14
|
'so.trooper.local-bridge',
|
|
@@ -33,6 +37,7 @@ Options:
|
|
|
33
37
|
--yes Run non-interactively with sensible defaults
|
|
34
38
|
--keep-data Preserve local workspace/config data during uninstall
|
|
35
39
|
--remove-app Also remove Trooper.app when uninstalling, if it is writable
|
|
40
|
+
--skip-app Install the local runtime only; do not install/open Trooper.app
|
|
36
41
|
--print-command Print the installer command without running it
|
|
37
42
|
--dry-run Same as --print-command
|
|
38
43
|
-h, --help Show this help
|
|
@@ -49,7 +54,7 @@ function parseArgs(argv) {
|
|
|
49
54
|
}
|
|
50
55
|
const [key, inlineValue] = arg.split('=', 2);
|
|
51
56
|
const name = key.replace(/^-+/, '');
|
|
52
|
-
if (['help', 'h', 'dry-run', 'print-command', 'yes', 'y', 'keep-data', 'remove-app'].includes(name)) {
|
|
57
|
+
if (['help', 'h', 'dry-run', 'print-command', 'yes', 'y', 'keep-data', 'remove-app', 'skip-app'].includes(name)) {
|
|
53
58
|
args[name] = true;
|
|
54
59
|
continue;
|
|
55
60
|
}
|
|
@@ -86,7 +91,110 @@ function localToken(prefix) {
|
|
|
86
91
|
return `${prefix}_${randomBytes(24).toString('base64url')}`;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
function
|
|
94
|
+
function decodeShellEnvValue(value = '') {
|
|
95
|
+
const normalized = String(value || '').trim();
|
|
96
|
+
if (normalized.startsWith("'") && normalized.endsWith("'")) return normalized.slice(1, -1);
|
|
97
|
+
return normalized.replace(/\\([\\ :/?&=])/g, '$1');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readExistingLocalHostBinding() {
|
|
101
|
+
if (process.platform !== 'darwin') return null;
|
|
102
|
+
const installIdPath = join(homedir(), 'Library', 'Application Support', 'Trooper', 'install-id');
|
|
103
|
+
const envPath = join(homedir(), 'Library', 'Application Support', 'Trooper', 'runtime', 'trooper-local-host.env');
|
|
104
|
+
const persistedHostDeviceId = existsSync(installIdPath)
|
|
105
|
+
? String(readFileSync(installIdPath, 'utf8') || '').trim()
|
|
106
|
+
: '';
|
|
107
|
+
if (!existsSync(envPath)) {
|
|
108
|
+
return persistedHostDeviceId ? {
|
|
109
|
+
envPath,
|
|
110
|
+
orgId: '',
|
|
111
|
+
hostDeviceId: persistedHostDeviceId,
|
|
112
|
+
apiUrl: DEFAULT_API_URL,
|
|
113
|
+
token: '',
|
|
114
|
+
paired: false,
|
|
115
|
+
} : null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const values = {};
|
|
119
|
+
for (const line of readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
120
|
+
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
121
|
+
if (match) values[match[1]] = decodeShellEnvValue(match[2]);
|
|
122
|
+
}
|
|
123
|
+
if (!values.ORG_ID) return null;
|
|
124
|
+
return {
|
|
125
|
+
envPath,
|
|
126
|
+
orgId: values.ORG_ID,
|
|
127
|
+
hostDeviceId: persistedHostDeviceId || values.TROOPER_INSTALLATION_ID || values.HOST_DEVICE_ID || '',
|
|
128
|
+
apiUrl: values.API_URL || DEFAULT_API_URL,
|
|
129
|
+
token: values.BRIDGE_AUTH_TOKEN || values.GATEWAY_TOKEN || '',
|
|
130
|
+
paired: values.ORG_ID !== 'local-unpaired',
|
|
131
|
+
};
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildLocalMacAppInstallCommand() {
|
|
138
|
+
return `install_or_open_trooper_app() {
|
|
139
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
140
|
+
local app_path=""
|
|
141
|
+
for candidate in "$HOME/Applications/Trooper.app" "/Applications/Trooper.app"; do
|
|
142
|
+
if [[ -d "$candidate" ]]; then
|
|
143
|
+
app_path="$candidate"
|
|
144
|
+
break
|
|
145
|
+
fi
|
|
146
|
+
done
|
|
147
|
+
|
|
148
|
+
if [[ -z "$app_path" ]] && /usr/bin/open -Ra "Trooper" >/dev/null 2>&1; then
|
|
149
|
+
echo "Opening Trooper desktop app..."
|
|
150
|
+
/usr/bin/open -a "Trooper"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [[ -n "$app_path" ]]; then
|
|
155
|
+
echo "Opening Trooper desktop app..."
|
|
156
|
+
/usr/bin/open "$app_path"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
echo "Installing Trooper desktop app..."
|
|
161
|
+
local dmg_url="\${TROOPER_MAC_APP_DMG_URL:-${TROOPER_MAC_APP_DMG_URL}}"
|
|
162
|
+
local tmp_dmg mount_dir app_src install_root
|
|
163
|
+
tmp_dmg="$(/usr/bin/mktemp -t trooper-macos.XXXXXX).dmg"
|
|
164
|
+
mount_dir="$(/usr/bin/mktemp -d -t trooper-dmg.XXXXXX)"
|
|
165
|
+
install_root="$HOME/Applications"
|
|
166
|
+
|
|
167
|
+
cleanup_trooper_dmg() {
|
|
168
|
+
/usr/bin/hdiutil detach "$mount_dir" -quiet >/dev/null 2>&1 || true
|
|
169
|
+
/bin/rm -f "$tmp_dmg"
|
|
170
|
+
/bin/rm -rf "$mount_dir"
|
|
171
|
+
}
|
|
172
|
+
trap cleanup_trooper_dmg EXIT
|
|
173
|
+
|
|
174
|
+
/usr/bin/curl -fL --retry 3 --retry-delay 2 "$dmg_url" -o "$tmp_dmg"
|
|
175
|
+
/usr/bin/hdiutil attach "$tmp_dmg" -mountpoint "$mount_dir" -nobrowse -quiet
|
|
176
|
+
app_src="$(/usr/bin/find "$mount_dir" -maxdepth 1 -name "Trooper.app" -type d | /usr/bin/head -1)"
|
|
177
|
+
if [[ -z "$app_src" ]]; then
|
|
178
|
+
echo "Trooper.app was not found inside the downloaded DMG."
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
/bin/mkdir -p "$install_root"
|
|
183
|
+
/bin/rm -rf "$install_root/Trooper.app"
|
|
184
|
+
/usr/bin/ditto "$app_src" "$install_root/Trooper.app"
|
|
185
|
+
/usr/bin/hdiutil detach "$mount_dir" -quiet >/dev/null 2>&1 || true
|
|
186
|
+
trap - EXIT
|
|
187
|
+
/bin/rm -f "$tmp_dmg"
|
|
188
|
+
/bin/rm -rf "$mount_dir"
|
|
189
|
+
|
|
190
|
+
echo "Trooper desktop app installed at: $install_root/Trooper.app"
|
|
191
|
+
/usr/bin/open "$install_root/Trooper.app"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
install_or_open_trooper_app`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildLocalMacInstallCommand({ apiUrl, installApp = true } = {}) {
|
|
90
198
|
const hostSuffix = randomBytes(6).toString('hex');
|
|
91
199
|
const env = {
|
|
92
200
|
TROOPER_LOCAL_HOST: '1',
|
|
@@ -101,10 +209,12 @@ function buildLocalMacInstallCommand({ apiUrl }) {
|
|
|
101
209
|
TUNNEL_PROVIDER: 'local',
|
|
102
210
|
};
|
|
103
211
|
return [
|
|
212
|
+
'set -euo pipefail',
|
|
104
213
|
buildExportCommand(env),
|
|
105
214
|
`curl -fsSL ${shellSingleQuote(LOCAL_MAC_SETUP_SCRIPT_URL)} -o /tmp/trooper-local-mac-host.sh`,
|
|
106
215
|
'chmod +x /tmp/trooper-local-mac-host.sh',
|
|
107
216
|
'bash /tmp/trooper-local-mac-host.sh',
|
|
217
|
+
installApp ? buildLocalMacAppInstallCommand() : '',
|
|
108
218
|
].join('\n');
|
|
109
219
|
}
|
|
110
220
|
|
|
@@ -122,6 +232,26 @@ GROUP_ID="\$(/usr/bin/id -g)"
|
|
|
122
232
|
|
|
123
233
|
echo "Uninstalling Trooper local host from this Mac..."
|
|
124
234
|
|
|
235
|
+
if [[ -f "$TROOPER_HOME/trooper-local-host.env" ]]; then
|
|
236
|
+
set -a
|
|
237
|
+
source "$TROOPER_HOME/trooper-local-host.env"
|
|
238
|
+
set +a
|
|
239
|
+
if [[ -n "\${ORG_ID:-}" && "\${ORG_ID:-}" != "local-unpaired" && -n "\${BRIDGE_AUTH_TOKEN:-}" ]]; then
|
|
240
|
+
RELEASE_API="\${API_URL:-${DEFAULT_API_URL}}"
|
|
241
|
+
RELEASE_BODY="$(ORG_ID="$ORG_ID" HOST_DEVICE_ID="\${TROOPER_INSTALLATION_ID:-\${HOST_DEVICE_ID:-}}" BRIDGE_AUTH_TOKEN="$BRIDGE_AUTH_TOKEN" /usr/bin/python3 - <<'PY'
|
|
242
|
+
import json, os
|
|
243
|
+
print(json.dumps({
|
|
244
|
+
"token": os.environ.get("BRIDGE_AUTH_TOKEN", ""),
|
|
245
|
+
"hostDeviceId": os.environ.get("HOST_DEVICE_ID", ""),
|
|
246
|
+
}, separators=(",", ":")))
|
|
247
|
+
PY
|
|
248
|
+
)"
|
|
249
|
+
/usr/bin/curl -fsS -X POST "$RELEASE_API/api/organizations/$ORG_ID/local-host/release" \
|
|
250
|
+
-H 'Content-Type: application/json' --data "$RELEASE_BODY" >/dev/null \
|
|
251
|
+
|| echo "Warning: Trooper could not release the online workspace binding. Local removal will continue."
|
|
252
|
+
fi
|
|
253
|
+
fi
|
|
254
|
+
|
|
125
255
|
ROOT_OWNED_PATHS=()
|
|
126
256
|
for path in "$TROOPER_HOME" "$TROOPER_PARENT_DIR/install-local-host.command" "$PLIST_DIR"/so.trooper.local-*.plist; do
|
|
127
257
|
if [[ -e "$path" && ! -O "$path" ]]; then
|
|
@@ -174,14 +304,14 @@ fi
|
|
|
174
304
|
echo "Docker, Colima, Homebrew, Node, and npm were left installed."`;
|
|
175
305
|
}
|
|
176
306
|
|
|
177
|
-
async function fetchSetupGuide({ apiUrl, token, platform }) {
|
|
307
|
+
async function fetchSetupGuide({ apiUrl, token, platform, hostDeviceId = '' }) {
|
|
178
308
|
const res = await fetch(`${apiUrl}/api/local-host/setup`, {
|
|
179
309
|
method: 'POST',
|
|
180
310
|
headers: {
|
|
181
311
|
'Content-Type': 'application/json',
|
|
182
312
|
'x-trooper-setup-token': token,
|
|
183
313
|
},
|
|
184
|
-
body: JSON.stringify({ token, platform }),
|
|
314
|
+
body: JSON.stringify({ token, platform, hostDeviceId }),
|
|
185
315
|
});
|
|
186
316
|
const data = await res.json().catch(async () => ({ message: await res.text() }));
|
|
187
317
|
if (!res.ok) {
|
|
@@ -261,7 +391,21 @@ async function main() {
|
|
|
261
391
|
console.error('Tokenless local install is currently available for macOS. Open Trooper to prepare a paired installer for this platform.');
|
|
262
392
|
process.exit(1);
|
|
263
393
|
}
|
|
264
|
-
const
|
|
394
|
+
const existingBinding = readExistingLocalHostBinding();
|
|
395
|
+
if (existingBinding?.paired) {
|
|
396
|
+
console.log(`This Mac already hosts Trooper workspace ${existingBinding.orgId}.`);
|
|
397
|
+
console.log('The existing local workspace was left unchanged. Opening Trooper...');
|
|
398
|
+
if (args['print-command'] || args['dry-run']) {
|
|
399
|
+
console.log('\n' + buildLocalMacAppInstallCommand());
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
runShellCommand(buildLocalMacAppInstallCommand(), platform, {
|
|
403
|
+
failureLabel: 'Trooper desktop app',
|
|
404
|
+
successMessage: '\nTrooper desktop app is ready.',
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const installCommand = buildLocalMacInstallCommand({ apiUrl, installApp: !args['skip-app'] });
|
|
265
409
|
console.log('Starting Trooper local host setup on this Mac...');
|
|
266
410
|
if (args['print-command'] || args['dry-run']) {
|
|
267
411
|
console.log('\n' + installCommand);
|
|
@@ -269,13 +413,27 @@ async function main() {
|
|
|
269
413
|
}
|
|
270
414
|
runShellCommand(installCommand, platform, {
|
|
271
415
|
failureLabel: 'Installer',
|
|
272
|
-
successMessage: '
|
|
416
|
+
successMessage: args['skip-app']
|
|
417
|
+
? '\nTrooper local host is installed. Open Trooper to connect this Mac to your workspace.'
|
|
418
|
+
: '\nTrooper local host is installed. Trooper desktop app is ready.',
|
|
273
419
|
});
|
|
274
420
|
return;
|
|
275
421
|
}
|
|
276
422
|
|
|
423
|
+
const existingBinding = readExistingLocalHostBinding();
|
|
277
424
|
console.log(`Preparing Trooper local setup from ${apiUrl}...`);
|
|
278
|
-
const guide = await fetchSetupGuide({
|
|
425
|
+
const guide = await fetchSetupGuide({
|
|
426
|
+
apiUrl,
|
|
427
|
+
token,
|
|
428
|
+
platform,
|
|
429
|
+
hostDeviceId: existingBinding?.hostDeviceId || '',
|
|
430
|
+
});
|
|
431
|
+
if (existingBinding?.paired && existingBinding.orgId !== guide.orgId) {
|
|
432
|
+
console.error(`This Mac already hosts Trooper workspace ${existingBinding.orgId}.`);
|
|
433
|
+
console.error(`Trooper will not replace it with workspace ${guide.orgId}.`);
|
|
434
|
+
console.error('Open the existing workspace, use another computer, or run `npx -y trooper-cli uninstall --yes` first.');
|
|
435
|
+
process.exit(73);
|
|
436
|
+
}
|
|
279
437
|
const label = guide.organizationName ? ` for ${guide.organizationName}` : '';
|
|
280
438
|
console.log(`Starting Trooper ${guide.platform || platform} local host setup${label}.`);
|
|
281
439
|
|