trooper-cli 0.1.0

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/bin/trooper.js +193 -0
  2. package/package.json +16 -0
package/bin/trooper.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { randomBytes } from 'node:crypto';
5
+
6
+ const DEFAULT_API_URL = 'https://trooper-production.up.railway.app';
7
+ const LOCAL_MAC_SETUP_SCRIPT_URL = 'https://raw.githubusercontent.com/absurdfounder/trooper-bridge/main/setup-local-mac-host.sh';
8
+
9
+ function printHelp() {
10
+ console.log(`Trooper CLI
11
+
12
+ Usage:
13
+ npx -y trooper-cli onboard --yes
14
+ npx -y trooper-cli onboard --yes --token <setup-token>
15
+
16
+ Aliases:
17
+ setup Same as onboard
18
+
19
+ Options:
20
+ --token <token> Optional short-lived setup token from Trooper for workspace pairing
21
+ --api <url> Trooper API URL (defaults to production)
22
+ --platform <name> macos, windows, or linux (defaults to this computer)
23
+ --yes Run non-interactively with sensible defaults
24
+ --print-command Print the installer command without running it
25
+ --dry-run Same as --print-command
26
+ -h, --help Show this help
27
+ `);
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const args = { _: [] };
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ if (!arg.startsWith('-')) {
35
+ args._.push(arg);
36
+ continue;
37
+ }
38
+ const [key, inlineValue] = arg.split('=', 2);
39
+ const name = key.replace(/^-+/, '');
40
+ if (['help', 'h', 'dry-run', 'print-command', 'yes', 'y'].includes(name)) {
41
+ args[name] = true;
42
+ continue;
43
+ }
44
+ const nextValue = inlineValue ?? argv[i + 1];
45
+ if (inlineValue == null) i += 1;
46
+ args[name] = nextValue || '';
47
+ }
48
+ return args;
49
+ }
50
+
51
+ function detectPlatform() {
52
+ if (process.platform === 'win32') return 'windows';
53
+ if (process.platform === 'darwin') return 'macos';
54
+ return 'linux';
55
+ }
56
+
57
+ function normalizeApiUrl(value) {
58
+ const raw = String(value || DEFAULT_API_URL).trim().replace(/\/+$/, '');
59
+ if (!raw) return DEFAULT_API_URL;
60
+ return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
61
+ }
62
+
63
+ function shellSingleQuote(value) {
64
+ return `'${String(value ?? '').replace(/'/g, "'\"'\"'")}'`;
65
+ }
66
+
67
+ function buildExportCommand(env) {
68
+ return Object.entries(env)
69
+ .map(([key, value]) => `export ${key}=${shellSingleQuote(value)}`)
70
+ .join('\n');
71
+ }
72
+
73
+ function localToken(prefix) {
74
+ return `${prefix}_${randomBytes(24).toString('base64url')}`;
75
+ }
76
+
77
+ function buildLocalMacInstallCommand({ apiUrl }) {
78
+ const hostSuffix = randomBytes(6).toString('hex');
79
+ const env = {
80
+ TROOPER_LOCAL_HOST: '1',
81
+ TROOPER_LOCAL_MAC_HOST: '1',
82
+ TROOPER_LOCAL_UNPAIRED: '1',
83
+ ORG_ID: 'local-unpaired',
84
+ API_URL: apiUrl,
85
+ HOST_DEVICE_ID: `mac-local-${hostSuffix}`,
86
+ GATEWAY_TOKEN: localToken('gw'),
87
+ BRIDGE_AUTH_TOKEN: localToken('bridge'),
88
+ BROWSER_MODE: 'managed',
89
+ TUNNEL_PROVIDER: 'local',
90
+ };
91
+ return [
92
+ buildExportCommand(env),
93
+ `curl -fsSL ${shellSingleQuote(LOCAL_MAC_SETUP_SCRIPT_URL)} -o /tmp/trooper-local-mac-host.sh`,
94
+ 'chmod +x /tmp/trooper-local-mac-host.sh',
95
+ 'bash /tmp/trooper-local-mac-host.sh',
96
+ ].join('\n');
97
+ }
98
+
99
+ async function fetchSetupGuide({ apiUrl, token, platform }) {
100
+ const res = await fetch(`${apiUrl}/api/local-host/setup`, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ 'x-trooper-setup-token': token,
105
+ },
106
+ body: JSON.stringify({ token, platform }),
107
+ });
108
+ const data = await res.json().catch(async () => ({ message: await res.text() }));
109
+ if (!res.ok) {
110
+ const message = data?.message || data?.error || `Trooper setup failed with HTTP ${res.status}`;
111
+ const error = new Error(message);
112
+ error.statusCode = res.status;
113
+ throw error;
114
+ }
115
+ if (!data?.installCommand) {
116
+ throw new Error('Trooper did not return an installer command.');
117
+ }
118
+ return data;
119
+ }
120
+
121
+ function runInstallCommand(command, platform, { successMessage = '' } = {}) {
122
+ const isWindows = platform === 'windows' || process.platform === 'win32';
123
+ const child = isWindows
124
+ ? spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command], { stdio: 'inherit' })
125
+ : spawn(process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash', ['-lc', command], { stdio: 'inherit' });
126
+
127
+ child.on('error', (error) => {
128
+ console.error(`Could not start installer: ${error.message}`);
129
+ process.exit(1);
130
+ });
131
+ child.on('exit', (code, signal) => {
132
+ if (signal) {
133
+ console.error(`Installer stopped by signal ${signal}`);
134
+ process.exit(1);
135
+ }
136
+ if ((code ?? 0) === 0 && successMessage) {
137
+ console.log(successMessage);
138
+ }
139
+ process.exit(code ?? 0);
140
+ });
141
+ }
142
+
143
+ async function main() {
144
+ const args = parseArgs(process.argv.slice(2));
145
+ const command = args._[0] || 'help';
146
+ if (args.help || args.h || command === 'help') {
147
+ printHelp();
148
+ return;
149
+ }
150
+ if (!['setup', 'onboard'].includes(command)) {
151
+ console.error(`Unknown command: ${command}`);
152
+ printHelp();
153
+ process.exit(1);
154
+ }
155
+
156
+ const apiUrl = normalizeApiUrl(args.api || process.env.TROOPER_API_URL);
157
+ const platform = String(args.platform || process.env.TROOPER_SETUP_PLATFORM || detectPlatform()).trim().toLowerCase();
158
+ const token = String(args.token || args['setup-token'] || process.env.TROOPER_SETUP_TOKEN || '').trim();
159
+
160
+ if (!token) {
161
+ if (platform !== 'macos') {
162
+ console.error('Tokenless local install is currently available for macOS. Open Trooper to prepare a paired installer for this platform.');
163
+ process.exit(1);
164
+ }
165
+ const installCommand = buildLocalMacInstallCommand({ apiUrl });
166
+ console.log('Starting Trooper local host setup on this Mac...');
167
+ if (args['print-command'] || args['dry-run']) {
168
+ console.log('\n' + installCommand);
169
+ return;
170
+ }
171
+ runInstallCommand(installCommand, platform, {
172
+ successMessage: '\nTrooper local host is installed. Open Trooper to connect this Mac to your workspace.',
173
+ });
174
+ return;
175
+ }
176
+
177
+ console.log(`Preparing Trooper local setup from ${apiUrl}...`);
178
+ const guide = await fetchSetupGuide({ apiUrl, token, platform });
179
+ const label = guide.organizationName ? ` for ${guide.organizationName}` : '';
180
+ console.log(`Starting Trooper ${guide.platform || platform} local host setup${label}.`);
181
+
182
+ if (args['print-command'] || args['dry-run']) {
183
+ console.log('\n' + guide.installCommand);
184
+ return;
185
+ }
186
+
187
+ runInstallCommand(guide.installCommand, platform);
188
+ }
189
+
190
+ main().catch((error) => {
191
+ console.error(error?.message || String(error));
192
+ process.exit(1);
193
+ });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "trooper-cli",
3
+ "version": "0.1.0",
4
+ "description": "Trooper local host setup CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "trooper": "bin/trooper.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "UNLICENSED"
16
+ }