stacktape 3.5.7 → 3.5.8

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/stacktape.js +206 -29
  2. package/package.json +1 -1
package/bin/stacktape.js CHANGED
@@ -14,7 +14,11 @@ const {
14
14
  accessSync,
15
15
  constants,
16
16
  unlinkSync,
17
- readFileSync
17
+ readFileSync,
18
+ writeFileSync,
19
+ readdirSync,
20
+ rmSync,
21
+ statSync
18
22
  } = require('node:fs');
19
23
  const { get: httpsGet } = require('node:https');
20
24
  const { platform, arch, homedir } = require('node:os');
@@ -33,6 +37,18 @@ const PLATFORM_MAP = {
33
37
  'linux-x64-musl': { fileName: 'alpine.tar.gz', extract: extractTarGz }
34
38
  };
35
39
 
40
+ const REQUIRED_HELPER_LAMBDA_PREFIXES = [
41
+ 'stacktapeServiceLambda',
42
+ 'cdnOriginRequestLambda',
43
+ 'cdnOriginResponseLambda',
44
+ 'batchJobTriggerLambda'
45
+ ];
46
+
47
+ const INSTALL_MARKER_FILE_NAME = '.stacktape-install.json';
48
+ const INSTALL_LOCK_DIR_SUFFIX = '.stacktape-install.lock';
49
+ const LOCK_WAIT_TIMEOUT_MS = 120000;
50
+ const STALE_LOCK_TIMEOUT_MS = 300000;
51
+
36
52
  // ANSI color codes
37
53
  const colors = {
38
54
  reset: '\x1B[0m',
@@ -200,55 +216,207 @@ async function ensureBinary() {
200
216
 
201
217
  const binaryName = platform() === 'win32' ? 'stacktape.exe' : 'stacktape';
202
218
 
219
+ const localCacheDir = join(__dirname, '..', 'bin');
220
+
203
221
  let cacheDir;
204
222
  try {
205
- const localDir = join(__dirname, '..', 'bin');
206
- mkdirSync(localDir, { recursive: true });
207
- accessSync(localDir, constants.W_OK);
208
- cacheDir = localDir;
223
+ mkdirSync(localCacheDir, { recursive: true });
224
+ accessSync(localCacheDir, constants.W_OK);
225
+ cacheDir = localCacheDir;
209
226
  } catch {
210
227
  cacheDir = join(homedir(), '.stacktape', 'bin', version);
211
228
  }
212
229
 
213
230
  const binaryPath = join(cacheDir, binaryName);
231
+ const preserveLauncherScript = cacheDir === localCacheDir;
214
232
 
215
- if (existsSync(binaryPath)) {
216
- return binaryPath;
217
- }
233
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
218
234
 
219
- console.info(`${colors.dim}Installing Stacktape ${version} for ${platformKey}...${colors.reset}`);
235
+ const lockDirPath = `${cacheDir}${INSTALL_LOCK_DIR_SUFFIX}`;
220
236
 
221
- mkdirSync(cacheDir, { recursive: true });
237
+ const isHelperLambdasCacheComplete = () => {
238
+ const helperLambdasDir = join(cacheDir, 'helper-lambdas');
239
+ if (!existsSync(helperLambdasDir)) {
240
+ return false;
241
+ }
222
242
 
223
- const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}/${platformInfo.fileName}`;
224
- const archivePath = join(cacheDir, platformInfo.fileName);
243
+ try {
244
+ const files = readdirSync(helperLambdasDir);
245
+ return REQUIRED_HELPER_LAMBDA_PREFIXES.every((prefix) =>
246
+ files.some((fileName) => fileName.startsWith(`${prefix}-`) && fileName.endsWith('.zip'))
247
+ );
248
+ } catch {
249
+ return false;
250
+ }
251
+ };
225
252
 
226
- try {
227
- console.info(`${colors.dim}Downloading from GitHub releases...${colors.reset}`);
228
- await downloadFile(downloadUrl, archivePath);
253
+ const isCacheValid = () => {
254
+ if (!existsSync(binaryPath)) {
255
+ return false;
256
+ }
229
257
 
230
- console.info(`${colors.dim}Extracting...${colors.reset}`);
231
- await platformInfo.extract(archivePath, cacheDir);
258
+ if (!isHelperLambdasCacheComplete()) {
259
+ return false;
260
+ }
232
261
 
233
- setExecutablePermissions(cacheDir);
262
+ const markerPath = join(cacheDir, INSTALL_MARKER_FILE_NAME);
263
+ if (!existsSync(markerPath)) {
264
+ return false;
265
+ }
234
266
 
235
- unlinkSync(archivePath);
267
+ try {
268
+ const parsedMarker = JSON.parse(readFileSync(markerPath, 'utf8'));
269
+ return parsedMarker.version === version && parsedMarker.platformKey === platformKey;
270
+ } catch {
271
+ return false;
272
+ }
273
+ };
236
274
 
237
- if (!existsSync(binaryPath)) {
238
- throw new Error(`Binary not found after extraction: ${binaryPath}`);
275
+ const writeInstallMarker = () => {
276
+ const markerPath = join(cacheDir, INSTALL_MARKER_FILE_NAME);
277
+ writeFileSync(
278
+ markerPath,
279
+ JSON.stringify(
280
+ {
281
+ version,
282
+ platformKey,
283
+ helperLambdas: REQUIRED_HELPER_LAMBDA_PREFIXES,
284
+ installedAt: new Date().toISOString()
285
+ },
286
+ null,
287
+ 2
288
+ )
289
+ );
290
+ };
291
+
292
+ const cleanupExtractedCache = () => {
293
+ if (!existsSync(cacheDir)) {
294
+ return;
295
+ }
296
+
297
+ try {
298
+ const entries = readdirSync(cacheDir);
299
+ for (const entry of entries) {
300
+ if (preserveLauncherScript && entry === 'stacktape.js') {
301
+ continue;
302
+ }
303
+ rmSync(join(cacheDir, entry), { recursive: true, force: true });
304
+ }
305
+ } catch {
306
+ // Ignore cleanup errors and try reinstall anyway
307
+ }
308
+ };
309
+
310
+ const acquireInstallLock = async () => {
311
+ const start = Date.now();
312
+ while (true) {
313
+ try {
314
+ mkdirSync(lockDirPath);
315
+ return;
316
+ } catch (error) {
317
+ if (error.code !== 'EEXIST') {
318
+ throw error;
319
+ }
320
+
321
+ try {
322
+ const lockStats = statSync(lockDirPath);
323
+ const lockAge = Date.now() - lockStats.mtimeMs;
324
+ if (lockAge > STALE_LOCK_TIMEOUT_MS) {
325
+ rmSync(lockDirPath, { recursive: true, force: true });
326
+ continue;
327
+ }
328
+ } catch {
329
+ // Lock directory disappeared between checks
330
+ }
331
+
332
+ if (Date.now() - start > LOCK_WAIT_TIMEOUT_MS) {
333
+ throw new Error('Timed out waiting for Stacktape binary installation lock');
334
+ }
335
+
336
+ await sleep(200);
337
+ }
338
+ }
339
+ };
340
+
341
+ const releaseInstallLock = () => {
342
+ try {
343
+ rmSync(lockDirPath, { recursive: true, force: true });
344
+ } catch {
345
+ // Ignore release lock errors
239
346
  }
347
+ };
348
+
349
+ const installBinary = async () => {
350
+ console.info(`${colors.dim}Installing Stacktape ${version} for ${platformKey}...${colors.reset}`);
351
+
352
+ mkdirSync(cacheDir, { recursive: true });
353
+
354
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}/${platformInfo.fileName}`;
355
+ const archivePath = join(cacheDir, `.download-${Date.now()}-${platformInfo.fileName}`);
356
+
357
+ try {
358
+ console.info(`${colors.dim}Downloading from GitHub releases...${colors.reset}`);
359
+ await downloadFile(downloadUrl, archivePath);
360
+
361
+ console.info(`${colors.dim}Extracting...${colors.reset}`);
362
+ await platformInfo.extract(archivePath, cacheDir);
240
363
 
241
- printLogo();
364
+ setExecutablePermissions(cacheDir);
242
365
 
366
+ unlinkSync(archivePath);
367
+
368
+ if (!existsSync(binaryPath)) {
369
+ throw new Error(`Binary not found after extraction: ${binaryPath}`);
370
+ }
371
+
372
+ if (!isHelperLambdasCacheComplete()) {
373
+ throw new Error('Incomplete installation: helper lambdas were not extracted correctly');
374
+ }
375
+
376
+ writeInstallMarker();
377
+ printLogo();
378
+
379
+ return binaryPath;
380
+ } catch (error) {
381
+ try {
382
+ if (existsSync(archivePath)) {
383
+ unlinkSync(archivePath);
384
+ }
385
+ } catch {
386
+ // Ignore archive cleanup errors
387
+ }
388
+ throw error;
389
+ }
390
+ };
391
+
392
+ if (isCacheValid()) {
243
393
  return binaryPath;
244
- } catch (error) {
245
- console.error(`
246
- ${colors.red}Error installing Stacktape:${colors.reset}
247
- ${error.message}
394
+ }
248
395
 
249
- You can also install Stacktape directly using:
250
- ${getManualInstallCommand(platformKey)}`);
251
- process.exit(1);
396
+ await acquireInstallLock();
397
+
398
+ try {
399
+ if (isCacheValid()) {
400
+ return binaryPath;
401
+ }
402
+
403
+ let lastError;
404
+ for (let attempt = 1; attempt <= 2; attempt++) {
405
+ try {
406
+ cleanupExtractedCache();
407
+ return await installBinary();
408
+ } catch (error) {
409
+ lastError = error;
410
+ cleanupExtractedCache();
411
+ if (attempt < 2) {
412
+ console.info(`\n${colors.dim}Retrying installation (${attempt + 1}/2)...${colors.reset}`);
413
+ }
414
+ }
415
+ }
416
+
417
+ throw lastError;
418
+ } finally {
419
+ releaseInstallLock();
252
420
  }
253
421
  }
254
422
 
@@ -269,6 +437,7 @@ function getGlobalBinaryPathIfVersionMatches() {
269
437
  const globalDir = join(homedir(), '.stacktape', 'bin');
270
438
  const globalBinaryPath = join(globalDir, binaryName);
271
439
  const releaseDataPath = join(globalDir, 'release-data.json');
440
+ const globalHelperLambdasDir = join(globalDir, 'helper-lambdas');
272
441
 
273
442
  if (!existsSync(globalBinaryPath) || !existsSync(releaseDataPath)) {
274
443
  return null;
@@ -279,6 +448,14 @@ function getGlobalBinaryPathIfVersionMatches() {
279
448
  if (version !== PACKAGE_VERSION) {
280
449
  return null;
281
450
  }
451
+
452
+ const files = readdirSync(globalHelperLambdasDir);
453
+ const hasAllHelperLambdas = REQUIRED_HELPER_LAMBDA_PREFIXES.every((prefix) =>
454
+ files.some((fileName) => fileName.startsWith(`${prefix}-`) && fileName.endsWith('.zip'))
455
+ );
456
+ if (!hasAllHelperLambdas) {
457
+ return null;
458
+ }
282
459
  } catch {
283
460
  return null;
284
461
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stacktape",
3
- "version": "3.5.7",
3
+ "version": "3.5.8",
4
4
  "description": "PaaS 2.0 that deploys to your own AWS account.",
5
5
  "author": "Stacktape team <support@stacktape.com>",
6
6
  "license": "MIT",