stacktape 2.23.0 → 2.24.0-beta.25

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.
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Stacktape CLI launcher
5
+ * Downloads and caches the platform-specific binary on first run
6
+ */
7
+
8
+ const { spawnSync, execSync } = require('node:child_process');
9
+ const { createWriteStream, existsSync, chmodSync, mkdirSync } = require('node:fs');
10
+ const { get: httpsGet } = require('node:https');
11
+ const { platform, arch, homedir } = require('node:os');
12
+ const { join } = require('node:path');
13
+
14
+ // Get version from package.json
15
+ const PACKAGE_VERSION = require('../package.json').version;
16
+
17
+ const GITHUB_REPO = 'stacktape/stacktape';
18
+
19
+ // Platform detection and mapping
20
+ const PLATFORM_MAP = {
21
+ 'win32-x64': { fileName: 'windows.zip', extract: extractZip },
22
+ 'linux-x64': { fileName: 'linux.tar.gz', extract: extractTarGz },
23
+ 'linux-arm64': { fileName: 'linux-arm.tar.gz', extract: extractTarGz },
24
+ 'darwin-x64': { fileName: 'macos.tar.gz', extract: extractTarGz },
25
+ 'darwin-arm64': { fileName: 'macos-arm.tar.gz', extract: extractTarGz },
26
+ 'linux-x64-musl': { fileName: 'alpine.tar.gz', extract: extractTarGz }
27
+ };
28
+
29
+ /**
30
+ * Detects the current platform
31
+ */
32
+ function detectPlatform() {
33
+ const currentPlatform = platform();
34
+ const currentArch = arch();
35
+
36
+ // Detect Alpine Linux (uses musl instead of glibc)
37
+ if (currentPlatform === 'linux' && currentArch === 'x64') {
38
+ try {
39
+ const ldd = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });
40
+ if (ldd.includes('musl')) {
41
+ return 'linux-x64-musl';
42
+ }
43
+ } catch {
44
+ // If ldd fails, assume glibc
45
+ }
46
+ }
47
+
48
+ const platformKey = `${currentPlatform}-${currentArch}`;
49
+
50
+ if (!PLATFORM_MAP[platformKey]) {
51
+ console.error(`Error: Unsupported platform ${currentPlatform}-${currentArch}`);
52
+ console.error('Stacktape binaries are available for:');
53
+ Object.keys(PLATFORM_MAP).forEach((key) => {
54
+ console.error(` - ${key}`);
55
+ });
56
+ process.exit(1);
57
+ }
58
+
59
+ return platformKey;
60
+ }
61
+
62
+ /**
63
+ * Downloads a file from a URL with retry logic
64
+ */
65
+ async function downloadFile(url, destPath, retries = 3) {
66
+ for (let i = 0; i < retries; i++) {
67
+ try {
68
+ await new Promise((resolve, reject) => {
69
+ const fileStream = createWriteStream(destPath);
70
+
71
+ httpsGet(url, (response) => {
72
+ // Follow redirects
73
+ if (response.statusCode === 301 || response.statusCode === 302) {
74
+ httpsGet(response.headers.location, (redirectResponse) => {
75
+ if (redirectResponse.statusCode !== 200) {
76
+ reject(new Error(`Failed to download: ${redirectResponse.statusCode}`));
77
+ return;
78
+ }
79
+
80
+ const totalBytes = Number.parseInt(redirectResponse.headers['content-length'], 10);
81
+ let downloadedBytes = 0;
82
+
83
+ redirectResponse.on('data', (chunk) => {
84
+ downloadedBytes += chunk.length;
85
+ const percent = totalBytes ? ((downloadedBytes / totalBytes) * 100).toFixed(1) : '?';
86
+ process.stdout.write(`\rDownloading... ${percent}%`);
87
+ });
88
+
89
+ redirectResponse.pipe(fileStream);
90
+ fileStream.on('finish', () => {
91
+ fileStream.close();
92
+ process.stdout.write('\n');
93
+ resolve();
94
+ });
95
+ }).on('error', reject);
96
+ return;
97
+ }
98
+
99
+ if (response.statusCode !== 200) {
100
+ reject(new Error(`Failed to download: ${response.statusCode}`));
101
+ return;
102
+ }
103
+
104
+ const totalBytes = Number.parseInt(response.headers['content-length'], 10);
105
+ let downloadedBytes = 0;
106
+
107
+ response.on('data', (chunk) => {
108
+ downloadedBytes += chunk.length;
109
+ const percent = totalBytes ? ((downloadedBytes / totalBytes) * 100).toFixed(1) : '?';
110
+ process.stdout.write(`\rDownloading... ${percent}%`);
111
+ });
112
+
113
+ response.pipe(fileStream);
114
+ fileStream.on('finish', () => {
115
+ fileStream.close();
116
+ process.stdout.write('\n');
117
+ resolve();
118
+ });
119
+ }).on('error', reject);
120
+ });
121
+
122
+ return; // Success
123
+ } catch (error) {
124
+ if (i === retries - 1) {
125
+ throw error;
126
+ }
127
+ console.info(`\nRetrying download (${i + 1}/${retries})...`);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Extracts a tar.gz archive
134
+ */
135
+ async function extractTarGz(archivePath, destDir) {
136
+ const tar = require('tar');
137
+ await tar.x({
138
+ file: archivePath,
139
+ cwd: destDir
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Extracts a zip archive
145
+ */
146
+ async function extractZip(archivePath, destDir) {
147
+ const AdmZip = require('adm-zip');
148
+ const zip = new AdmZip(archivePath);
149
+ zip.extractAllTo(destDir, true);
150
+ }
151
+
152
+ /**
153
+ * Sets executable permissions on Unix systems
154
+ */
155
+ function setExecutablePermissions(binDir) {
156
+ if (platform() === 'win32') {
157
+ return; // Windows doesn't need chmod
158
+ }
159
+
160
+ const executables = [
161
+ join(binDir, 'stacktape'),
162
+ join(binDir, 'esbuild', 'exec'),
163
+ join(binDir, 'session-manager-plugin', 'smp'),
164
+ join(binDir, 'pack', 'pack'),
165
+ join(binDir, 'nixpacks', 'nixpacks')
166
+ ];
167
+
168
+ for (const exe of executables) {
169
+ if (existsSync(exe)) {
170
+ try {
171
+ chmodSync(exe, 0o755);
172
+ } catch {
173
+ // Ignore chmod errors
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Ensures the binary is downloaded and cached
181
+ */
182
+ async function ensureBinary() {
183
+ const platformKey = detectPlatform();
184
+ const platformInfo = PLATFORM_MAP[platformKey];
185
+ const version = PACKAGE_VERSION;
186
+
187
+ // Cache directory: ~/.stacktape/bin/{version}/
188
+ const cacheDir = join(homedir(), '.stacktape', 'bin', version);
189
+ const binaryName = platform() === 'win32' ? 'stacktape.exe' : 'stacktape';
190
+ const binaryPath = join(cacheDir, binaryName);
191
+
192
+ // Check if binary is already cached
193
+ if (existsSync(binaryPath)) {
194
+ return binaryPath;
195
+ }
196
+
197
+ console.info(`Installing Stacktape ${version} for ${platformKey}...`);
198
+
199
+ // Create cache directory
200
+ if (!existsSync(cacheDir)) {
201
+ mkdirSync(cacheDir, { recursive: true });
202
+ }
203
+
204
+ // Download URL
205
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}/${platformInfo.fileName}`;
206
+ const archivePath = join(cacheDir, platformInfo.fileName);
207
+
208
+ try {
209
+ // Download the archive
210
+ console.info(`Downloading from ${downloadUrl}...`);
211
+ await downloadFile(downloadUrl, archivePath);
212
+
213
+ // Extract the archive
214
+ console.info('Extracting...');
215
+ await platformInfo.extract(archivePath, cacheDir);
216
+
217
+ // Set executable permissions
218
+ setExecutablePermissions(cacheDir);
219
+
220
+ // Remove the archive
221
+ const { unlinkSync } = require('node:fs');
222
+ unlinkSync(archivePath);
223
+
224
+ // Verify the binary exists
225
+ if (!existsSync(binaryPath)) {
226
+ throw new Error(`Binary not found after extraction: ${binaryPath}`);
227
+ }
228
+
229
+ console.info(`✓ Stacktape ${version} installed successfully`);
230
+
231
+ return binaryPath;
232
+ } catch (error) {
233
+ console.error(`
234
+ Error installing Stacktape:
235
+ ${error.message}
236
+
237
+ You can also install Stacktape directly using:
238
+ ${getManualInstallCommand(platformKey)}`);
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Gets the manual installation command for the platform
245
+ */
246
+ function getManualInstallCommand(platformKey) {
247
+ const commands = {
248
+ 'win32-x64': 'iwr https://installs.stacktape.com/windows.ps1 -useb | iex',
249
+ 'linux-x64': 'curl -L https://installs.stacktape.com/linux.sh | sh',
250
+ 'linux-arm64': 'curl -L https://installs.stacktape.com/linux-arm.sh | sh',
251
+ 'linux-x64-musl': 'curl -L https://installs.stacktape.com/alpine.sh | sh',
252
+ 'darwin-x64': 'curl -L https://installs.stacktape.com/macos.sh | sh',
253
+ 'darwin-arm64': 'curl -L https://installs.stacktape.com/macos-arm.sh | sh'
254
+ };
255
+ return commands[platformKey] || 'See https://docs.stacktape.com for installation instructions';
256
+ }
257
+
258
+ /**
259
+ * Main execution
260
+ */
261
+ async function main() {
262
+ try {
263
+ const binaryPath = await ensureBinary();
264
+ const args = process.argv.slice(2);
265
+
266
+ const result = spawnSync(binaryPath, args, {
267
+ stdio: 'inherit',
268
+ env: process.env
269
+ });
270
+
271
+ if (result.error) {
272
+ console.error(`Error executing Stacktape binary: ${result.error.message}`);
273
+ process.exit(1);
274
+ }
275
+
276
+ process.exit(result.status || 0);
277
+ } catch (error) {
278
+ console.error('Unexpected error:', error.message);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ main();