@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 +1 -1
- package/src/cli/commands/update.ts +58 -18
- package/src/cli/index.ts +21 -2
- package/src/handler/assets.ts +15 -0
- package/src/storage/index.ts +2 -0
- package/src/storage/s3.ts +10 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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':
|
package/src/handler/assets.ts
CHANGED
|
@@ -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) {
|
package/src/storage/index.ts
CHANGED
|
@@ -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
|
}
|