@stacksjs/ts-cloud 0.5.7 → 0.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.
- package/dist/bin/cli.js +628 -626
- package/dist/deploy/serverless-app.d.ts +20 -0
- package/dist/index.js +48 -24
- package/package.json +3 -3
|
@@ -43,6 +43,8 @@ export declare function resolveServerlessFunctions(config: CloudConfig, environm
|
|
|
43
43
|
cli: string;
|
|
44
44
|
};
|
|
45
45
|
};
|
|
46
|
+
/** Resolve secrets from Secrets Manager into a flat env map. */
|
|
47
|
+
export declare function resolveSecrets(app: ServerlessAppConfig, region: string): Promise<Record<string, string>>;
|
|
46
48
|
/**
|
|
47
49
|
* Derive framework env vars (DB/Redis hosts) from the deployed stack outputs so
|
|
48
50
|
* a Laravel app connects to the Aurora/RDS-Proxy/ElastiCache resources the
|
|
@@ -72,6 +74,24 @@ export declare function rollbackServerlessApp(config: CloudConfig, environment:
|
|
|
72
74
|
export declare function setMaintenance(config: CloudConfig, environment: EnvironmentType, enabled: boolean, bypassSecret?: string): Promise<void>;
|
|
73
75
|
/** Invoke the CLI function with an arbitrary command (e.g. `cloud command "migrate"`). */
|
|
74
76
|
export declare function runRemoteCommand(config: CloudConfig, environment: EnvironmentType, command: string): Promise<string>;
|
|
77
|
+
/** One recorded `cloud command` invocation. */
|
|
78
|
+
export interface CommandRecord {
|
|
79
|
+
id: number;
|
|
80
|
+
command: string;
|
|
81
|
+
timestamp: string;
|
|
82
|
+
status: 'ok' | 'error';
|
|
83
|
+
output: string;
|
|
84
|
+
}
|
|
85
|
+
/** Run a command on the CLI function AND record it to the history log (best-effort). */
|
|
86
|
+
export declare function runAndRecordCommand(config: CloudConfig, environment: EnvironmentType, command: string): Promise<{
|
|
87
|
+
id: number;
|
|
88
|
+
status: 'ok' | 'error';
|
|
89
|
+
output: string;
|
|
90
|
+
}>;
|
|
91
|
+
/** Return the recorded command history (most-recent last). */
|
|
92
|
+
export declare function listCommandHistory(config: CloudConfig, environment: EnvironmentType): Promise<CommandRecord[]>;
|
|
93
|
+
/** Look up one history record by id (or the most recent when id is omitted). */
|
|
94
|
+
export declare function getCommandRecord(config: CloudConfig, environment: EnvironmentType, id?: number): Promise<CommandRecord | undefined>;
|
|
75
95
|
/**
|
|
76
96
|
* Run a SQL statement against a (private, in-VPC) serverless database via the CLI
|
|
77
97
|
* function — no bastion needed. Requires the `tscloud/serverless` PHP bridge
|
package/dist/index.js
CHANGED
|
@@ -26187,34 +26187,39 @@ function composeServerlessAppTemplate(opts) {
|
|
|
26187
26187
|
}
|
|
26188
26188
|
if (app.warm && app.warm > 0) {
|
|
26189
26189
|
const TARGETS_PER_RULE = 5;
|
|
26190
|
-
const
|
|
26191
|
-
|
|
26192
|
-
for (
|
|
26193
|
-
const
|
|
26194
|
-
|
|
26195
|
-
|
|
26190
|
+
const fnResource = { http: "HttpFunction", queue: "QueueFunction", cli: "CliFunction" };
|
|
26191
|
+
const warmModes = (app.warmFunctions ?? ["http"]).filter((m) => m !== "queue" || hasQueue);
|
|
26192
|
+
for (const mode of warmModes) {
|
|
26193
|
+
const cap = mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
26194
|
+
const ruleCount = Math.ceil(app.warm / TARGETS_PER_RULE);
|
|
26195
|
+
let warmed = 0;
|
|
26196
|
+
for (let r = 0;r < ruleCount; r++) {
|
|
26197
|
+
const targets = Math.min(TARGETS_PER_RULE, app.warm - warmed);
|
|
26198
|
+
resources[`Warmer${cap}Rule${r}`] = {
|
|
26199
|
+
Type: "AWS::Events::Rule",
|
|
26200
|
+
Properties: {
|
|
26201
|
+
Name: `${slug}-${environment}-warmer-${mode}-${r}`,
|
|
26202
|
+
ScheduleExpression: "rate(5 minutes)",
|
|
26203
|
+
State: "ENABLED",
|
|
26204
|
+
Targets: Array.from({ length: targets }, (_, i) => ({
|
|
26205
|
+
Id: `warm-${mode}-${r}-${i}`,
|
|
26206
|
+
Arn: Fn2.getAtt(fnResource[mode], "Arn"),
|
|
26207
|
+
Input: JSON.stringify({ warmer: true })
|
|
26208
|
+
}))
|
|
26209
|
+
}
|
|
26210
|
+
};
|
|
26211
|
+
warmed += targets;
|
|
26212
|
+
}
|
|
26213
|
+
resources[`Warmer${cap}Permission`] = {
|
|
26214
|
+
Type: "AWS::Lambda::Permission",
|
|
26196
26215
|
Properties: {
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26200
|
-
|
|
26201
|
-
Id: `warm-${r}-${i}`,
|
|
26202
|
-
Arn: Fn2.getAtt("HttpFunction", "Arn"),
|
|
26203
|
-
Input: JSON.stringify({ warmer: true })
|
|
26204
|
-
}))
|
|
26216
|
+
FunctionName: Fn2.ref(fnResource[mode]),
|
|
26217
|
+
Action: "lambda:InvokeFunction",
|
|
26218
|
+
Principal: "events.amazonaws.com",
|
|
26219
|
+
SourceArn: Fn2.sub(`arn:aws:events:\${AWS::Region}:\${AWS::AccountId}:rule/${slug}-${environment}-warmer-${mode}-*`)
|
|
26205
26220
|
}
|
|
26206
26221
|
};
|
|
26207
|
-
warmed += targets;
|
|
26208
26222
|
}
|
|
26209
|
-
resources.WarmerPermission = {
|
|
26210
|
-
Type: "AWS::Lambda::Permission",
|
|
26211
|
-
Properties: {
|
|
26212
|
-
FunctionName: Fn2.ref("HttpFunction"),
|
|
26213
|
-
Action: "lambda:InvokeFunction",
|
|
26214
|
-
Principal: "events.amazonaws.com",
|
|
26215
|
-
SourceArn: Fn2.sub(`arn:aws:events:\${AWS::Region}:\${AWS::AccountId}:rule/${slug}-${environment}-warmer-*`)
|
|
26216
|
-
}
|
|
26217
|
-
};
|
|
26218
26223
|
}
|
|
26219
26224
|
if (cacheEnabled) {
|
|
26220
26225
|
resources.CacheTable = {
|
|
@@ -26807,6 +26812,8 @@ function parseRecordBody(body) {
|
|
|
26807
26812
|
function createQueueHandler(handler8) {
|
|
26808
26813
|
return async (event) => {
|
|
26809
26814
|
const batchItemFailures = [];
|
|
26815
|
+
if (event.warmer)
|
|
26816
|
+
return { batchItemFailures };
|
|
26810
26817
|
if (!handler8)
|
|
26811
26818
|
return { batchItemFailures };
|
|
26812
26819
|
for (const record of event.Records ?? []) {
|
|
@@ -26821,6 +26828,8 @@ function createQueueHandler(handler8) {
|
|
|
26821
26828
|
}
|
|
26822
26829
|
function createCliHandler(handler8) {
|
|
26823
26830
|
return async (event) => {
|
|
26831
|
+
if (event.warmer)
|
|
26832
|
+
return { statusCode: 0, output: "warm" };
|
|
26824
26833
|
if (!handler8)
|
|
26825
26834
|
return { statusCode: 501, output: "No CLI handler configured" };
|
|
26826
26835
|
return handler8(event);
|
|
@@ -58022,6 +58031,9 @@ function colorize(text, color) {
|
|
|
58022
58031
|
function success(message) {
|
|
58023
58032
|
console.log(`${colors.green}✓${colors.reset} ${message}`);
|
|
58024
58033
|
}
|
|
58034
|
+
function error(message) {
|
|
58035
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`);
|
|
58036
|
+
}
|
|
58025
58037
|
function warn(message) {
|
|
58026
58038
|
console.warn(`${colors.yellow}⚠${colors.reset} ${message}`);
|
|
58027
58039
|
}
|
|
@@ -83301,6 +83313,16 @@ async function resolveSecrets(app, region) {
|
|
|
83301
83313
|
const sm = new SecretsManagerClient(region);
|
|
83302
83314
|
const out = {};
|
|
83303
83315
|
const entries = Array.isArray(app.secrets) ? app.secrets.map((name) => ({ envName: name.split("/").pop().toUpperCase().replace(/[^A-Z0-9_]/g, "_"), secretId: name })) : Object.entries(app.secrets).map(([envName, secretId]) => ({ envName, secretId }));
|
|
83316
|
+
if (Array.isArray(app.secrets)) {
|
|
83317
|
+
const byName = new Map;
|
|
83318
|
+
for (const { envName, secretId } of entries)
|
|
83319
|
+
byName.set(envName, [...byName.get(envName) ?? [], secretId]);
|
|
83320
|
+
const clashes = [...byName.entries()].filter(([, ids]) => ids.length > 1);
|
|
83321
|
+
if (clashes.length) {
|
|
83322
|
+
const detail = clashes.map(([name, ids]) => `${name} ← ${ids.join(", ")}`).join("; ");
|
|
83323
|
+
throw new Error(`serverless secrets: multiple secrets map to the same env var (${detail}). Use the map form { ENV_NAME: 'secret/id' } to disambiguate.`);
|
|
83324
|
+
}
|
|
83325
|
+
}
|
|
83304
83326
|
for (const { envName, secretId } of entries) {
|
|
83305
83327
|
const value = await sm.getSecretValue({ SecretId: secretId });
|
|
83306
83328
|
const str = value.SecretString ?? "";
|
|
@@ -83586,6 +83608,8 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83586
83608
|
LogType: "Tail"
|
|
83587
83609
|
});
|
|
83588
83610
|
if (res.FunctionError) {
|
|
83611
|
+
error("Deploy hook failed — the new build is ALREADY LIVE.");
|
|
83612
|
+
info(` Roll back with: cloud serverless:rollback --env ${environment}`);
|
|
83589
83613
|
throw new Error(`Deploy hook failed (${command}): ${res.Payload}`);
|
|
83590
83614
|
}
|
|
83591
83615
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/ts-cloud",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
5
5
|
"description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"test": "bun test"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@ts-cloud/aws-types": "0.5.
|
|
93
|
-
"@ts-cloud/core": "0.5.
|
|
92
|
+
"@ts-cloud/aws-types": "0.5.8",
|
|
93
|
+
"@ts-cloud/core": "0.5.8",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|