@everystack/cli 0.1.0 → 0.2.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.
- package/LICENSE +681 -0
- package/package.json +42 -38
- package/src/cli/aws.ts +60 -13
- package/src/cli/commands/cache.ts +24 -3
- package/src/cli/commands/logs.ts +4 -4
- package/src/cli/commands/update.ts +57 -8
- package/src/cli/config.ts +3 -0
- package/src/cli/discover-distribution.ts +36 -0
- package/src/cli/discover.ts +15 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/utils/export.ts +19 -0
- package/src/handler/index.ts +1 -0
- package/src/plugin.ts +123 -0
- package/src/storage/index.ts +3 -3
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"src",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
|
-
"type": "module",
|
|
14
13
|
"exports": {
|
|
15
14
|
".": {
|
|
16
15
|
"types": "./src/index.ts",
|
|
@@ -27,30 +26,39 @@
|
|
|
27
26
|
"./handler": {
|
|
28
27
|
"types": "./src/handler/index.ts",
|
|
29
28
|
"default": "./src/handler/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"./plugin": {
|
|
31
|
+
"types": "./src/plugin.ts",
|
|
32
|
+
"default": "./src/plugin.ts"
|
|
30
33
|
}
|
|
31
34
|
},
|
|
32
35
|
"bin": {
|
|
33
36
|
"everystack": "./src/cli/index.ts"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-cloudfront-keyvaluestore": "3.1045.0",
|
|
40
|
+
"@aws-sdk/signature-v4a": "3.1031.0",
|
|
36
41
|
"glob": "13.0.6",
|
|
37
|
-
"node-forge": "
|
|
38
|
-
"structured-headers": "
|
|
39
|
-
"tsx": "
|
|
42
|
+
"node-forge": "1.4.0",
|
|
43
|
+
"structured-headers": "1.0.1",
|
|
44
|
+
"tsx": "4.21.0"
|
|
40
45
|
},
|
|
41
46
|
"peerDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@aws-sdk/client-cloudfront
|
|
44
|
-
"@aws-sdk/client-cloudwatch-logs": "
|
|
45
|
-
"@aws-sdk/client-lambda": "
|
|
46
|
-
"@aws-sdk/client-s3": "
|
|
47
|
-
"@aws-sdk/
|
|
48
|
-
"drizzle-orm": "
|
|
49
|
-
"expo-updates": "
|
|
50
|
-
"react": "
|
|
51
|
-
"react-native": "
|
|
47
|
+
"@everystack/server": ">=0.1.0",
|
|
48
|
+
"@aws-sdk/client-cloudfront": "3.1045.0",
|
|
49
|
+
"@aws-sdk/client-cloudwatch-logs": "3.1047.0",
|
|
50
|
+
"@aws-sdk/client-lambda": "3.1045.0",
|
|
51
|
+
"@aws-sdk/client-s3": "3.1045.0",
|
|
52
|
+
"@aws-sdk/s3-request-presigner": "3.1045.0",
|
|
53
|
+
"drizzle-orm": "0.41.0",
|
|
54
|
+
"expo-updates": "55.0.21",
|
|
55
|
+
"react": "19.2.6",
|
|
56
|
+
"react-native": "0.83.6"
|
|
52
57
|
},
|
|
53
58
|
"peerDependenciesMeta": {
|
|
59
|
+
"@everystack/server": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
54
62
|
"@aws-sdk/client-cloudfront": {
|
|
55
63
|
"optional": true
|
|
56
64
|
},
|
|
@@ -63,12 +71,6 @@
|
|
|
63
71
|
"@aws-sdk/client-lambda": {
|
|
64
72
|
"optional": true
|
|
65
73
|
},
|
|
66
|
-
"@aws-sdk/client-cloudfront-keyvaluestore": {
|
|
67
|
-
"optional": true
|
|
68
|
-
},
|
|
69
|
-
"@aws-sdk/signature-v4a": {
|
|
70
|
-
"optional": true
|
|
71
|
-
},
|
|
72
74
|
"expo-updates": {
|
|
73
75
|
"optional": true
|
|
74
76
|
},
|
|
@@ -77,27 +79,29 @@
|
|
|
77
79
|
},
|
|
78
80
|
"react-native": {
|
|
79
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@aws-sdk/s3-request-presigner": {
|
|
84
|
+
"optional": true
|
|
80
85
|
}
|
|
81
86
|
},
|
|
82
87
|
"devDependencies": {
|
|
83
|
-
"@aws-sdk/client-cloudfront": "
|
|
84
|
-
"@aws-sdk/client-
|
|
85
|
-
"@aws-sdk/client-
|
|
86
|
-
"@aws-sdk/client-
|
|
87
|
-
"@aws-sdk/
|
|
88
|
-
"@
|
|
89
|
-
"@types/
|
|
90
|
-
"@types/node": "
|
|
91
|
-
"@types/
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"typescript": "^5.7.0"
|
|
88
|
+
"@aws-sdk/client-cloudfront": "3.1045.0",
|
|
89
|
+
"@aws-sdk/client-cloudwatch-logs": "3.1047.0",
|
|
90
|
+
"@aws-sdk/client-lambda": "3.1045.0",
|
|
91
|
+
"@aws-sdk/client-s3": "3.1045.0",
|
|
92
|
+
"@aws-sdk/s3-request-presigner": "3.1045.0",
|
|
93
|
+
"@types/jest": "29.5.14",
|
|
94
|
+
"@types/node": "22.19.18",
|
|
95
|
+
"@types/node-forge": "1.3.14",
|
|
96
|
+
"@types/react": "19.2.14",
|
|
97
|
+
"drizzle-orm": "0.41.0",
|
|
98
|
+
"jest": "29.7.0",
|
|
99
|
+
"react": "19.2.6",
|
|
100
|
+
"ts-jest": "29.4.9",
|
|
101
|
+
"typescript": "5.9.3"
|
|
98
102
|
},
|
|
99
103
|
"scripts": {
|
|
100
|
-
"test": "
|
|
104
|
+
"test": "jest",
|
|
101
105
|
"build": "tsc --build",
|
|
102
106
|
"lint": "tsc --noEmit"
|
|
103
107
|
}
|
package/src/cli/aws.ts
CHANGED
|
@@ -5,20 +5,23 @@
|
|
|
5
5
|
* Authentication uses the developer's IAM credentials (default credential chain).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let s3Client: InstanceType<typeof import('@aws-sdk/client-s3').S3Client> | null = null;
|
|
9
|
+
let lambdaClient: InstanceType<typeof import('@aws-sdk/client-lambda').LambdaClient> | null = null;
|
|
10
|
+
let kvsClient: InstanceType<typeof import('@aws-sdk/client-cloudfront-keyvaluestore').CloudFrontKeyValueStoreClient> | null = null;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!s3Client) s3Client = new S3Client({ region });
|
|
12
|
+
async function getS3(region: string) {
|
|
13
|
+
if (!s3Client) {
|
|
14
|
+
const { S3Client } = await import('@aws-sdk/client-s3');
|
|
15
|
+
s3Client = new S3Client({ region });
|
|
16
|
+
}
|
|
17
17
|
return s3Client;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function getLambda(region: string)
|
|
21
|
-
if (!lambdaClient)
|
|
20
|
+
async function getLambda(region: string) {
|
|
21
|
+
if (!lambdaClient) {
|
|
22
|
+
const { LambdaClient } = await import('@aws-sdk/client-lambda');
|
|
23
|
+
lambdaClient = new LambdaClient({ region });
|
|
24
|
+
}
|
|
22
25
|
return lambdaClient;
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -29,7 +32,8 @@ export async function uploadToS3(
|
|
|
29
32
|
body: Buffer | Uint8Array,
|
|
30
33
|
contentType: string,
|
|
31
34
|
): Promise<void> {
|
|
32
|
-
const client = getS3(region);
|
|
35
|
+
const client = await getS3(region);
|
|
36
|
+
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
33
37
|
await client.send(new PutObjectCommand({
|
|
34
38
|
Bucket: bucket,
|
|
35
39
|
Key: key,
|
|
@@ -43,7 +47,8 @@ export async function getFromS3(
|
|
|
43
47
|
bucket: string,
|
|
44
48
|
key: string,
|
|
45
49
|
): Promise<Buffer | null> {
|
|
46
|
-
const client = getS3(region);
|
|
50
|
+
const client = await getS3(region);
|
|
51
|
+
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
47
52
|
try {
|
|
48
53
|
const response = await client.send(new GetObjectCommand({
|
|
49
54
|
Bucket: bucket,
|
|
@@ -66,7 +71,8 @@ export async function invokeAction(
|
|
|
66
71
|
action: string,
|
|
67
72
|
payload: unknown,
|
|
68
73
|
): Promise<unknown> {
|
|
69
|
-
const client = getLambda(region);
|
|
74
|
+
const client = await getLambda(region);
|
|
75
|
+
const { InvokeCommand } = await import('@aws-sdk/client-lambda');
|
|
70
76
|
const response = await client.send(new InvokeCommand({
|
|
71
77
|
FunctionName: functionName,
|
|
72
78
|
Payload: new TextEncoder().encode(JSON.stringify({
|
|
@@ -86,6 +92,47 @@ export async function invokeAction(
|
|
|
86
92
|
return JSON.parse(new TextDecoder().decode(response.Payload));
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the CloudWatch log group for a Lambda function.
|
|
97
|
+
* Queries GetFunctionConfiguration for LoggingConfig.LogGroup, falling back to
|
|
98
|
+
* the conventional `/aws/lambda/{functionName}` pattern if not set.
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveLogGroup(
|
|
101
|
+
region: string,
|
|
102
|
+
functionName: string,
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
const client = await getLambda(region);
|
|
105
|
+
const { GetFunctionConfigurationCommand } = await import('@aws-sdk/client-lambda');
|
|
106
|
+
const config = await client.send(
|
|
107
|
+
new GetFunctionConfigurationCommand({ FunctionName: functionName })
|
|
108
|
+
);
|
|
109
|
+
return config.LoggingConfig?.LogGroup ?? `/aws/lambda/${functionName}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a CloudFront cache invalidation.
|
|
114
|
+
* Used by cache:purge --invalidate to clear edge-cached responses (e.g. stale 404s).
|
|
115
|
+
*/
|
|
116
|
+
export async function createInvalidation(
|
|
117
|
+
region: string,
|
|
118
|
+
distributionId: string,
|
|
119
|
+
paths: string[] = ['/*'],
|
|
120
|
+
): Promise<string | undefined> {
|
|
121
|
+
const { CloudFrontClient, CreateInvalidationCommand } = await import('@aws-sdk/client-cloudfront');
|
|
122
|
+
const client = new CloudFrontClient({ region });
|
|
123
|
+
const result = await client.send(new CreateInvalidationCommand({
|
|
124
|
+
DistributionId: distributionId,
|
|
125
|
+
InvalidationBatch: {
|
|
126
|
+
CallerReference: `cache-purge-${Date.now()}`,
|
|
127
|
+
Paths: {
|
|
128
|
+
Quantity: paths.length,
|
|
129
|
+
Items: paths,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
return result.Invalidation?.Id;
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
/**
|
|
90
137
|
* Write a versioning key to CloudFront KeyValueStore.
|
|
91
138
|
* KVS requires ETag for optimistic concurrency — DescribeKeyValueStore first.
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* No args → global epoch bump → all cached content refreshes.
|
|
5
5
|
* --origin api|media|web → per-origin epoch bump → only that origin refreshes.
|
|
6
6
|
* --path "/api/posts" → per-URL version bump → only that path refreshes.
|
|
7
|
+
* --invalidate → also create a CloudFront edge invalidation (for stale 404s).
|
|
7
8
|
*
|
|
8
|
-
* Writes directly to CloudFront KVS — no Lambda invoke
|
|
9
|
+
* Writes directly to CloudFront KVS — no Lambda invoke.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { resolveConfig } from '../config.js';
|
|
12
|
-
import { putKvsKey } from '../aws.js';
|
|
13
|
-
import { step, success, fail, info } from '../output.js';
|
|
13
|
+
import { putKvsKey, createInvalidation } from '../aws.js';
|
|
14
|
+
import { step, success, warn, fail, info } from '../output.js';
|
|
14
15
|
|
|
15
16
|
const VALID_ORIGINS = ['api', 'media', 'web'] as const;
|
|
16
17
|
type CacheOrigin = typeof VALID_ORIGINS[number];
|
|
@@ -69,4 +70,24 @@ export async function cachePurgeCommand(flags: Record<string, string>): Promise<
|
|
|
69
70
|
success(`Cache epoch updated to ${epoch}`);
|
|
70
71
|
info('All cached content will refresh on next request.');
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
// CloudFront edge invalidation (optional — clears cached responses without _v param)
|
|
75
|
+
if (flags.invalidate === 'true') {
|
|
76
|
+
const distributionId = flags['distribution-id'] || config.distributionId;
|
|
77
|
+
if (!distributionId) {
|
|
78
|
+
warn('No distributionId found. Add `distributionId` to sst.config.ts return block, or pass --distribution-id.');
|
|
79
|
+
info(' return { ..., distributionId: router.nodes.distribution.id }');
|
|
80
|
+
} else {
|
|
81
|
+
step('Creating CloudFront invalidation...');
|
|
82
|
+
try {
|
|
83
|
+
const invalidationPaths = flags.path ? [flags.path] : ['/*'];
|
|
84
|
+
const invalidationId = await createInvalidation(config.region, distributionId, invalidationPaths);
|
|
85
|
+
success(`CloudFront invalidation created: ${invalidationId || 'submitted'}`);
|
|
86
|
+
info('Edge caches will clear within 1-2 minutes.');
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
warn(`CloudFront invalidation failed: ${err.message}`);
|
|
89
|
+
info('Ensure your IAM user/role has cloudfront:CreateInvalidation permission.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
}
|
package/src/cli/commands/logs.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { resolveConfig } from '../config.js';
|
|
10
|
-
import { invokeAction } from '../aws.js';
|
|
10
|
+
import { invokeAction, resolveLogGroup } from '../aws.js';
|
|
11
11
|
import { step, success, fail, info } from '../output.js';
|
|
12
12
|
|
|
13
13
|
export async function logsErrorsCommand(flags: Record<string, string>): Promise<void> {
|
|
@@ -104,13 +104,13 @@ export async function logsTailCommand(flags: Record<string, string>): Promise<vo
|
|
|
104
104
|
|
|
105
105
|
try {
|
|
106
106
|
// Import AWS SDK dynamically
|
|
107
|
-
const { CloudWatchLogsClient, FilterLogEventsCommand
|
|
107
|
+
const { CloudWatchLogsClient, FilterLogEventsCommand } =
|
|
108
108
|
await import('@aws-sdk/client-cloudwatch-logs');
|
|
109
109
|
|
|
110
110
|
const client = new CloudWatchLogsClient({ region: config.region });
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
const logGroupName =
|
|
112
|
+
// Resolve actual log group from Lambda config (handles SST name changes)
|
|
113
|
+
const logGroupName = await resolveLogGroup(config.region, config.apiFunctionName);
|
|
114
114
|
|
|
115
115
|
// Parse since duration to milliseconds
|
|
116
116
|
const sinceMs = parseDuration(since);
|
|
@@ -8,7 +8,7 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { resolveConfig } from '../config.js';
|
|
9
9
|
import { uploadToS3, invokeAction } from '../aws.js';
|
|
10
10
|
import { step, success, warn, fail, info } from '../output.js';
|
|
11
|
-
import { exportApp } from '../utils/export.js';
|
|
11
|
+
import { exportApp, isDistStale } from '../utils/export.js';
|
|
12
12
|
import { walkDirectory } from '../utils/walk.js';
|
|
13
13
|
|
|
14
14
|
export interface UpdateFlags {
|
|
@@ -32,13 +32,26 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
32
32
|
|
|
33
33
|
if (!runtimeVersion) {
|
|
34
34
|
fail('No runtimeVersion found in app.json/app.config.js');
|
|
35
|
+
info('Add "runtimeVersion" to your app.json, e.g.:');
|
|
36
|
+
info(' { "expo": { "runtimeVersion": { "policy": "fingerprint" } } }');
|
|
37
|
+
info('See: https://docs.expo.dev/eas-update/runtime-versions/');
|
|
35
38
|
process.exit(1);
|
|
36
39
|
}
|
|
37
40
|
success(`Runtime version: ${runtimeVersion}`);
|
|
38
41
|
|
|
39
42
|
// Export if needed
|
|
40
43
|
const distDir = path.resolve('dist');
|
|
41
|
-
const
|
|
44
|
+
const distExists = await dirExists(distDir);
|
|
45
|
+
let shouldExport = flags.export === 'true' || !distExists;
|
|
46
|
+
|
|
47
|
+
// Check if dist/ is stale (source files newer than dist/)
|
|
48
|
+
if (!shouldExport && flags['skip-export'] !== 'true' && distExists) {
|
|
49
|
+
if (await isDistStale(distDir)) {
|
|
50
|
+
step('Source files changed since last export, re-exporting...');
|
|
51
|
+
shouldExport = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
if (shouldExport) {
|
|
43
56
|
step('Exporting app...');
|
|
44
57
|
await exportApp();
|
|
@@ -264,12 +277,43 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
264
277
|
success(`${uploaded}/${clientAssets.length} assets uploaded to s3://${config.clientBundlesBucket}/`);
|
|
265
278
|
}
|
|
266
279
|
|
|
267
|
-
// 4.
|
|
268
|
-
|
|
269
|
-
step('Registering release...');
|
|
280
|
+
// 4. Write release manifests directly to S3 (source of truth for SSR).
|
|
281
|
+
step('Writing release manifests...');
|
|
270
282
|
const updateId = crypto.createHash('sha256')
|
|
271
283
|
.update(`${channel}:${runtimeVersion}:${Date.now()}`)
|
|
272
284
|
.digest('hex');
|
|
285
|
+
const createdAt = new Date().toISOString();
|
|
286
|
+
const webManifest = JSON.stringify({
|
|
287
|
+
updateId,
|
|
288
|
+
runtimeVersion,
|
|
289
|
+
platform: 'web',
|
|
290
|
+
storagePrefix,
|
|
291
|
+
createdAt,
|
|
292
|
+
});
|
|
293
|
+
const manifestBody = Buffer.from(webManifest);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
await Promise.all([
|
|
297
|
+
// Versioned manifest: releases/{branch}/{rv}/{groupId}/web/manifest.json
|
|
298
|
+
uploadToS3(
|
|
299
|
+
config.region, config.updatesBucket,
|
|
300
|
+
`${storagePrefix}/manifest.json`, manifestBody, 'application/json',
|
|
301
|
+
),
|
|
302
|
+
// Channel pointer: {channel}/web/manifest.json (mutable, points to latest)
|
|
303
|
+
uploadToS3(
|
|
304
|
+
config.region, config.updatesBucket,
|
|
305
|
+
`${channel}/web/manifest.json`, manifestBody, 'application/json',
|
|
306
|
+
),
|
|
307
|
+
]);
|
|
308
|
+
success(`Manifests written (channel=${channel}, updateId=${updateId.slice(0, 12)}...)`);
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
fail(`Manifest write failed: ${err.message}`);
|
|
311
|
+
info('Ensure your IAM user/role has s3:PutObject on the Updates bucket.');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 5. Register release via Lambda invoke (optional — for DB mirroring only).
|
|
316
|
+
// The S3 manifests above are the source of truth for SSR resolution.
|
|
273
317
|
try {
|
|
274
318
|
const result: any = await invokeAction(
|
|
275
319
|
config.region,
|
|
@@ -286,12 +330,17 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
286
330
|
},
|
|
287
331
|
);
|
|
288
332
|
if (result?.error) {
|
|
289
|
-
|
|
333
|
+
info(`DB mirror skipped: ${result.error}`);
|
|
290
334
|
} else {
|
|
291
|
-
|
|
335
|
+
info(`Release mirrored to DB: channel=${result?.channel || channel}`);
|
|
292
336
|
}
|
|
293
337
|
} catch (err: any) {
|
|
294
|
-
|
|
338
|
+
// register-web is optional — the S3 manifests are the source of truth.
|
|
339
|
+
if (err.message?.includes('Unknown action')) {
|
|
340
|
+
info('Tip: add a "register-web" case to onAction for DB-backed release tracking.');
|
|
341
|
+
} else {
|
|
342
|
+
info(`DB mirror skipped: ${err.message}`);
|
|
343
|
+
}
|
|
295
344
|
}
|
|
296
345
|
|
|
297
346
|
// Clean up archive
|
package/src/cli/config.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CliConfig {
|
|
|
18
18
|
updatesBucket: string;
|
|
19
19
|
clientBundlesBucket: string;
|
|
20
20
|
kvsArn?: string;
|
|
21
|
+
distributionId?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface SstOutputs {
|
|
@@ -28,6 +29,7 @@ interface SstOutputs {
|
|
|
28
29
|
updatesBucket?: string;
|
|
29
30
|
clientBundlesBucket?: string;
|
|
30
31
|
kvsArn?: string;
|
|
32
|
+
distributionId?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export async function resolveConfig(stage?: string): Promise<CliConfig> {
|
|
@@ -94,5 +96,6 @@ export async function resolveConfig(stage?: string): Promise<CliConfig> {
|
|
|
94
96
|
updatesBucket: outputs.updatesBucket,
|
|
95
97
|
clientBundlesBucket: outputs.clientBundlesBucket,
|
|
96
98
|
kvsArn: outputs.kvsArn,
|
|
99
|
+
distributionId: outputs.distributionId,
|
|
97
100
|
};
|
|
98
101
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFront distribution discovery.
|
|
3
|
+
*
|
|
4
|
+
* Separated from discover.ts to avoid Jest ESM module resolution issues
|
|
5
|
+
* with the @aws-sdk/client-cloudfront ListDistributions types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find the CloudFront distribution for a stage by matching the Comment field.
|
|
10
|
+
* SST sets the Comment to include the app name and stage.
|
|
11
|
+
* Best-effort: returns undefined if not found.
|
|
12
|
+
*/
|
|
13
|
+
export async function discoverDistribution(
|
|
14
|
+
region: string,
|
|
15
|
+
prefix: string,
|
|
16
|
+
): Promise<string | undefined> {
|
|
17
|
+
const { CloudFrontClient, ListDistributionsCommand } = await import('@aws-sdk/client-cloudfront');
|
|
18
|
+
const client = new CloudFrontClient({ region });
|
|
19
|
+
const distNeedle = prefix.toLowerCase();
|
|
20
|
+
|
|
21
|
+
let marker: string | undefined;
|
|
22
|
+
do {
|
|
23
|
+
const res = await client.send(
|
|
24
|
+
new ListDistributionsCommand({ Marker: marker, MaxItems: 50 }),
|
|
25
|
+
);
|
|
26
|
+
for (const dist of res.DistributionList?.Items || []) {
|
|
27
|
+
const comment = (dist.Comment || '').toLowerCase();
|
|
28
|
+
if (comment.includes(distNeedle)) {
|
|
29
|
+
return dist.Id;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
marker = res.DistributionList?.IsTruncated ? res.DistributionList.NextMarker : undefined;
|
|
33
|
+
} while (marker);
|
|
34
|
+
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
package/src/cli/discover.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface DiscoveredConfig {
|
|
|
22
22
|
updatesBucket: string;
|
|
23
23
|
clientBundlesBucket: string;
|
|
24
24
|
kvsArn?: string;
|
|
25
|
+
distributionId?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +62,7 @@ export async function parseAppName(configPath?: string): Promise<string> {
|
|
|
61
62
|
|
|
62
63
|
interface CfFunctionResult {
|
|
63
64
|
kvsArn?: string;
|
|
65
|
+
distributionId?: string;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/**
|
|
@@ -240,6 +242,15 @@ export async function discoverConfig(
|
|
|
240
242
|
discoverBuckets(region, prefix),
|
|
241
243
|
]);
|
|
242
244
|
|
|
245
|
+
// Distribution discovery is best-effort (not critical for most CLI operations)
|
|
246
|
+
let distributionId: string | undefined;
|
|
247
|
+
try {
|
|
248
|
+
const { discoverDistribution } = await import('./discover-distribution.js');
|
|
249
|
+
distributionId = await discoverDistribution(region, prefix);
|
|
250
|
+
} catch {
|
|
251
|
+
// Non-fatal — only needed for cache:purge --invalidate
|
|
252
|
+
}
|
|
253
|
+
|
|
243
254
|
if (!bucketsResult.updatesBucket) {
|
|
244
255
|
throw new Error(
|
|
245
256
|
`Could not find S3 bucket "${prefix}updatesbucket-*". ` +
|
|
@@ -261,6 +272,7 @@ export async function discoverConfig(
|
|
|
261
272
|
updatesBucket: bucketsResult.updatesBucket,
|
|
262
273
|
clientBundlesBucket: bucketsResult.clientBundlesBucket,
|
|
263
274
|
kvsArn: cfResult.kvsArn,
|
|
275
|
+
distributionId,
|
|
264
276
|
};
|
|
265
277
|
}
|
|
266
278
|
|
|
@@ -277,6 +289,7 @@ interface CachedOutputs {
|
|
|
277
289
|
updatesBucket?: string;
|
|
278
290
|
clientBundlesBucket?: string;
|
|
279
291
|
kvsArn?: string;
|
|
292
|
+
distributionId?: string;
|
|
280
293
|
}
|
|
281
294
|
|
|
282
295
|
function cachePath(stage: string): string {
|
|
@@ -298,6 +311,7 @@ export async function getCachedConfig(stage: string): Promise<DiscoveredConfig |
|
|
|
298
311
|
updatesBucket: cached.updatesBucket,
|
|
299
312
|
clientBundlesBucket: cached.clientBundlesBucket,
|
|
300
313
|
kvsArn: cached.kvsArn,
|
|
314
|
+
distributionId: cached.distributionId,
|
|
301
315
|
};
|
|
302
316
|
} catch {
|
|
303
317
|
return null;
|
|
@@ -312,6 +326,7 @@ export async function setCachedConfig(stage: string, config: DiscoveredConfig):
|
|
|
312
326
|
updatesBucket: config.updatesBucket,
|
|
313
327
|
clientBundlesBucket: config.clientBundlesBucket,
|
|
314
328
|
kvsArn: config.kvsArn,
|
|
329
|
+
distributionId: config.distributionId,
|
|
315
330
|
};
|
|
316
331
|
try {
|
|
317
332
|
await fs.writeFile(cachePath(stage), JSON.stringify(data, null, 2));
|
package/src/cli/index.ts
CHANGED
|
@@ -131,8 +131,8 @@ function printHelp() {
|
|
|
131
131
|
everystack - CLI for Expo apps on everystack
|
|
132
132
|
|
|
133
133
|
Usage:
|
|
134
|
-
everystack update --branch <name> --message <msg> [--platform ios|android|web|all] [--stage <name>]
|
|
135
|
-
everystack update --channel <name> --message <msg> [--platform ios|android|web|all] [--stage <name>]
|
|
134
|
+
everystack update --branch <name> --message <msg> [--platform ios|android|web|all] [--stage <name>] [--skip-export]
|
|
135
|
+
everystack update --channel <name> --message <msg> [--platform ios|android|web|all] [--stage <name>] [--skip-export]
|
|
136
136
|
everystack db:migrate [--stage <name>] Run database migrations on deployed Lambda
|
|
137
137
|
everystack db:seed [--stage <name>] Seed database on deployed Lambda (dev only)
|
|
138
138
|
everystack db:reset [--stage <name>] Drop all schemas + re-run migrations (dev only)
|
|
@@ -143,6 +143,7 @@ Usage:
|
|
|
143
143
|
everystack cache:purge [--stage <name>] Bust all cached content (global epoch)
|
|
144
144
|
everystack cache:purge [--stage <name>] --origin api Bust API origin cache (api|media|web)
|
|
145
145
|
everystack cache:purge [--stage <name>] --path "/api/posts" Bust specific path cache
|
|
146
|
+
everystack cache:purge [--stage <name>] --invalidate Also create CloudFront edge invalidation
|
|
146
147
|
everystack diag [url] [--path /about] [--channel <name>] [--stage <name>] Diagnose page freshness
|
|
147
148
|
everystack diag [url] --hydration Analyze SSR hydration issues (fetches full HTML)
|
|
148
149
|
everystack analyze:ssr [--app ./app] Scan app code for SSR anti-patterns
|
package/src/cli/utils/export.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
|
|
3
5
|
export async function exportApp(): Promise<void> {
|
|
4
6
|
execSync('npx expo export --output-dir dist', {
|
|
@@ -6,3 +8,20 @@ export async function exportApp(): Promise<void> {
|
|
|
6
8
|
env: { ...process.env },
|
|
7
9
|
});
|
|
8
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if dist/ is stale by comparing its mtime against known source files.
|
|
14
|
+
* Returns true if any source file (app.json, app.config.js, app.config.ts, app/)
|
|
15
|
+
* was modified after dist/ was last written.
|
|
16
|
+
*/
|
|
17
|
+
export async function isDistStale(distDir: string, projectDir?: string): Promise<boolean> {
|
|
18
|
+
const baseDir = projectDir ?? process.cwd();
|
|
19
|
+
const distMtime = (await fs.stat(distDir)).mtimeMs;
|
|
20
|
+
const sourceChecks = ['app.json', 'app.config.js', 'app.config.ts', 'app'].map(async (f) => {
|
|
21
|
+
try {
|
|
22
|
+
const s = await fs.stat(path.resolve(baseDir, f));
|
|
23
|
+
return s.mtimeMs > distMtime;
|
|
24
|
+
} catch { return false; }
|
|
25
|
+
});
|
|
26
|
+
return (await Promise.all(sourceChecks)).some(Boolean);
|
|
27
|
+
}
|
package/src/handler/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { handleListChannels, handleCreateChannel, handleEditChannel } from './ch
|
|
|
8
8
|
|
|
9
9
|
export { handleRegisterWeb } from './publish-web';
|
|
10
10
|
export { registerMobileRelease } from './publish';
|
|
11
|
+
export { handleListChannels, handleCreateChannel } from './channels-crud';
|
|
11
12
|
export type { RegisterMobileParams, RegisterMobileResult, RegisterMobileAsset } from './publish';
|
|
12
13
|
export type { UpdatesHandlerOptions } from './types';
|
|
13
14
|
|