@everystack/cli 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
@@ -5,7 +5,7 @@ import zlib from 'node:zlib';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import { createWriteStream } from 'node:fs';
7
7
  import { spawn } from 'node:child_process';
8
- import { resolveConfig } from '../config.js';
8
+ import { resolveConfig, type CliConfig } from '../config.js';
9
9
  import { uploadToS3, invokeAction } from '../aws.js';
10
10
  import { step, success, warn, fail, info } from '../output.js';
11
11
  import { exportApp, isDistStale } from '../utils/export.js';
@@ -20,6 +20,38 @@ export interface UpdateFlags {
20
20
  }
21
21
 
22
22
  export async function updateCommand(flags: UpdateFlags & Record<string, string>): Promise<void> {
23
+ // Signal publish context so app.config.js can detect it's not a dev session.
24
+ // Without this, dotenv guards that skip .env.local for EAS_BUILD/EAS_UPDATE
25
+ // don't know to skip for everystack update, causing local dev overrides
26
+ // (e.g. OUTBOUND_HOST=localhost → LAN IP) to leak into the manifest.
27
+ process.env.EVERYSTACK_UPDATE = '1';
28
+
29
+ // Propagate --stage to ENVIRONMENT before app.config.js evaluation.
30
+ // Without this, the manifest snapshots whatever the local shell has
31
+ // (often unset → 'development'), baking wrong API URLs and feature
32
+ // flags into the OTA update. Only set if not already supplied.
33
+ if (flags.stage) {
34
+ process.env.ENVIRONMENT ||= flags.stage;
35
+ }
36
+
37
+ // When --stage is provided, resolve deployed config early so HOST_URL
38
+ // is correct before app.config.js evaluation and expo export.
39
+ // loadSstOutputs() reads .sst/outputs.json (last deploy, any stage)
40
+ // which may be from a different stage entirely.
41
+ let resolvedConfig: CliConfig | undefined;
42
+ if (flags.stage) {
43
+ try {
44
+ step('Resolving deployed config...');
45
+ resolvedConfig = await resolveConfig(flags.stage);
46
+ if (resolvedConfig.baseUrl) {
47
+ process.env.HOST_URL ||= resolvedConfig.baseUrl;
48
+ }
49
+ success(`Region: ${resolvedConfig.region}, Function: ${resolvedConfig.apiFunctionName}`);
50
+ } catch {
51
+ // Stage may not be deployed yet — HOST_URL falls back to loadSstOutputs
52
+ }
53
+ }
54
+
23
55
  const branch = flags.branch;
24
56
  const channel = flags.channel || branch || flags.stage || 'production';
25
57
  const message = flags.message || '';
@@ -82,15 +114,19 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
82
114
  // Mobile platforms — direct S3 upload + IAM-authed Lambda invoke (mirrors web path).
83
115
  // No HTTP publish endpoint, no shared bearer token, no WAF body-size limits.
84
116
  if (mobilePlatforms.length > 0) {
85
- step('Resolving deployed config...');
86
- let config;
87
- try {
88
- config = await resolveConfig(flags.stage);
89
- } catch (err: any) {
90
- fail(err.message);
91
- process.exit(1);
117
+ let config: CliConfig;
118
+ if (resolvedConfig) {
119
+ config = resolvedConfig;
120
+ } else {
121
+ step('Resolving deployed config...');
122
+ try {
123
+ config = await resolveConfig(flags.stage);
124
+ } catch (err: any) {
125
+ fail(err.message);
126
+ process.exit(1);
127
+ }
128
+ success(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
92
129
  }
93
- success(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
94
130
 
95
131
  for (const platform of mobilePlatforms) {
96
132
  const platformMetadata = metadata?.fileMetadata?.[platform];
@@ -197,16 +233,20 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
197
233
  if (!(await fileExists(routesJson))) {
198
234
  warn('Skipping web (no server export found — ensure app.json has "output": "server")');
199
235
  } else {
200
- // Resolve SST config for direct AWS access
201
- step('Resolving deployed config...');
202
- let config;
203
- try {
204
- config = await resolveConfig(flags.stage);
205
- } catch (err: any) {
206
- fail(err.message);
207
- process.exit(1);
236
+ // Resolve SST config for direct AWS access (reuse early resolution if available)
237
+ let config: CliConfig;
238
+ if (resolvedConfig) {
239
+ config = resolvedConfig;
240
+ } else {
241
+ step('Resolving deployed config...');
242
+ try {
243
+ config = await resolveConfig(flags.stage);
244
+ } catch (err: any) {
245
+ fail(err.message);
246
+ process.exit(1);
247
+ }
248
+ success(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
208
249
  }
209
- success(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
210
250
 
211
251
  // 1. Create tar+brotli archive of server bundle
212
252
  step('Creating server bundle archive...');
package/src/cli/index.ts CHANGED
@@ -38,10 +38,29 @@ function parseFlags(args: string[]): Record<string, string> {
38
38
  * Auto-detect HOST_URL from SST outputs.
39
39
  * SST writes .sst/outputs.json after every deploy with { routerUrl, apiUrl, ... }.
40
40
  * If HOST_URL isn't already set, use the routerUrl from the last deploy.
41
+ *
42
+ * When a stage is specified, checks the stage-specific cached config first.
43
+ * .sst/outputs.json reflects the last deploy (any stage) and may be wrong
44
+ * when publishing to a different stage.
41
45
  */
42
- async function loadSstOutputs(): Promise<void> {
46
+ async function loadSstOutputs(stage?: string): Promise<void> {
43
47
  if (process.env.HOST_URL) return;
44
48
 
49
+ // When --stage is provided, try stage-specific cache first
50
+ if (stage) {
51
+ const stagePath = path.resolve('.sst', `outputs.${stage}.json`);
52
+ try {
53
+ const raw = await fs.readFile(stagePath, 'utf8');
54
+ const cached = JSON.parse(raw);
55
+ if (cached.routerUrl) {
56
+ process.env.HOST_URL = cached.routerUrl;
57
+ return;
58
+ }
59
+ } catch {
60
+ // No cached config for this stage — fall through to default
61
+ }
62
+ }
63
+
45
64
  const outputsPath = path.resolve('.sst', 'outputs.json');
46
65
  try {
47
66
  const raw = await fs.readFile(outputsPath, 'utf8');
@@ -63,7 +82,7 @@ async function main() {
63
82
  const flags = parseFlags(args.slice(1));
64
83
 
65
84
  // Auto-detect deployed URL from SST outputs
66
- await loadSstOutputs();
85
+ await loadSstOutputs(flags.stage);
67
86
 
68
87
  switch (command) {
69
88
  case 'update':
@@ -11,6 +11,21 @@ export async function handleAssets(request: Request, options: UpdatesHandlerOpti
11
11
  });
12
12
  }
13
13
 
14
+ // Redirect to a presigned S3 URL instead of proxying the bytes through Lambda.
15
+ // Lambda function URLs cap responses at 6 MB; Hermes bytecode bundles exceed that.
16
+ if (options.storage.presignGet) {
17
+ const presignedUrl = await options.storage.presignGet(key);
18
+ return new Response(null, {
19
+ status: 302,
20
+ headers: {
21
+ location: presignedUrl,
22
+ 'cache-control': 'private, max-age=300',
23
+ },
24
+ });
25
+ }
26
+
27
+ // Fallback: direct streaming for non-S3 backends (local dev, filesystem).
28
+ // No Lambda payload limit applies in these environments.
14
29
  const result = await options.storage.get(key);
15
30
 
16
31
  if (!result) {
@@ -8,6 +8,8 @@ export interface StorageAdapter {
8
8
  delete(key: string): Promise<void>;
9
9
  /** Generate a presigned PUT URL. S3-only — returns null for other backends. */
10
10
  presignPut?(key: string, contentType: string, expiresIn?: number): Promise<string>;
11
+ /** Generate a presigned GET URL. S3-only — returns undefined for other backends. */
12
+ presignGet?(key: string, expiresIn?: number): Promise<string>;
11
13
  }
12
14
 
13
15
  export type StorageOptions =
package/src/storage/s3.ts CHANGED
@@ -122,4 +122,14 @@ export class S3StorageAdapter implements StorageAdapter {
122
122
  ContentType: contentType,
123
123
  }), { expiresIn });
124
124
  }
125
+
126
+ async presignGet(key: string, expiresIn: number = 3600): Promise<string> {
127
+ const { GetObjectCommand } = await import('@aws-sdk/client-s3');
128
+ const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
129
+ const client = await this.getClient();
130
+ return getSignedUrl(client, new GetObjectCommand({
131
+ Bucket: this.bucket,
132
+ Key: key,
133
+ }), { expiresIn });
134
+ }
125
135
  }