trooper-cli 0.1.6 → 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 CHANGED
@@ -27,6 +27,8 @@ On macOS, `npx -y trooper-cli` prepares:
27
27
 
28
28
  The local desktop plan does not require a Trooper cloud computer or Stripe checkout.
29
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
+
30
32
  ## Commands
31
33
 
32
34
  ### Install local runtime
@@ -72,6 +74,8 @@ npx -y --prefer-online trooper-cli@latest uninstall --yes
72
74
 
73
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.
74
76
 
77
+ When online, uninstall also releases this computer's workspace binding so it can be paired elsewhere.
78
+
75
79
  Keep local workspace/config data:
76
80
 
77
81
  ```bash
package/bin/trooper.js CHANGED
@@ -2,6 +2,9 @@
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';
@@ -88,6 +91,49 @@ function localToken(prefix) {
88
91
  return `${prefix}_${randomBytes(24).toString('base64url')}`;
89
92
  }
90
93
 
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
+
91
137
  function buildLocalMacAppInstallCommand() {
92
138
  return `install_or_open_trooper_app() {
93
139
  export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
@@ -186,6 +232,26 @@ GROUP_ID="\$(/usr/bin/id -g)"
186
232
 
187
233
  echo "Uninstalling Trooper local host from this Mac..."
188
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
+
189
255
  ROOT_OWNED_PATHS=()
190
256
  for path in "$TROOPER_HOME" "$TROOPER_PARENT_DIR/install-local-host.command" "$PLIST_DIR"/so.trooper.local-*.plist; do
191
257
  if [[ -e "$path" && ! -O "$path" ]]; then
@@ -238,14 +304,14 @@ fi
238
304
  echo "Docker, Colima, Homebrew, Node, and npm were left installed."`;
239
305
  }
240
306
 
241
- async function fetchSetupGuide({ apiUrl, token, platform }) {
307
+ async function fetchSetupGuide({ apiUrl, token, platform, hostDeviceId = '' }) {
242
308
  const res = await fetch(`${apiUrl}/api/local-host/setup`, {
243
309
  method: 'POST',
244
310
  headers: {
245
311
  'Content-Type': 'application/json',
246
312
  'x-trooper-setup-token': token,
247
313
  },
248
- body: JSON.stringify({ token, platform }),
314
+ body: JSON.stringify({ token, platform, hostDeviceId }),
249
315
  });
250
316
  const data = await res.json().catch(async () => ({ message: await res.text() }));
251
317
  if (!res.ok) {
@@ -325,6 +391,20 @@ async function main() {
325
391
  console.error('Tokenless local install is currently available for macOS. Open Trooper to prepare a paired installer for this platform.');
326
392
  process.exit(1);
327
393
  }
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
+ }
328
408
  const installCommand = buildLocalMacInstallCommand({ apiUrl, installApp: !args['skip-app'] });
329
409
  console.log('Starting Trooper local host setup on this Mac...');
330
410
  if (args['print-command'] || args['dry-run']) {
@@ -340,8 +420,20 @@ async function main() {
340
420
  return;
341
421
  }
342
422
 
423
+ const existingBinding = readExistingLocalHostBinding();
343
424
  console.log(`Preparing Trooper local setup from ${apiUrl}...`);
344
- const guide = await fetchSetupGuide({ apiUrl, token, platform });
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
+ }
345
437
  const label = guide.organizationName ? ` for ${guide.organizationName}` : '';
346
438
  console.log(`Starting Trooper ${guide.platform || platform} local host setup${label}.`);
347
439
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trooper-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Official Trooper CLI for installing and managing the local desktop AI runtime.",
5
5
  "type": "module",
6
6
  "bin": {