@stacksjs/buddy 0.70.88 → 0.70.91
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/cli.d.ts +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.d.ts +20 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.d.ts +26 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +141 -0
- package/dist/commands/deploy.d.ts +107 -0
- package/dist/commands/deploy.js +1492 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.js +915 -0
- package/dist/commands/dns.d.ts +2 -0
- package/dist/commands/dns.js +102 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +285 -0
- package/dist/commands/domains.d.ts +2 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.d.ts +2 -0
- package/dist/commands/email.js +524 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/extension.d.ts +8 -0
- package/dist/commands/extension.js +78 -0
- package/dist/commands/features.d.ts +180 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.d.ts +2 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.d.ts +2 -0
- package/dist/commands/http.js +33 -0
- package/dist/commands/index.d.ts +53 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +46 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.d.ts +4 -0
- package/dist/commands/mail.js +752 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.d.ts +2 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.d.ts +2 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.js +455 -0
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.d.ts +2 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.d.ts +2 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.d.ts +2 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +172 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +248 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.d.ts +2 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.d.ts +2 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.d.ts +25 -0
- package/dist/commands/serve.js +182 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +215 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +214 -0
- package/dist/commands/sms.d.ts +2 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.d.ts +2 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +188 -0
- package/dist/migrators/index.d.ts +9 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.d.ts +2 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.d.ts +52 -0
- package/dist/migrators/laravel/migrations.js +232 -0
- package/dist/migrators/laravel/models.d.ts +34 -0
- package/dist/migrators/laravel/models.js +134 -0
- package/dist/migrators/rails/index.d.ts +2 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.d.ts +44 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { intro, italic, log, onUnknownSubcommand, outro, prompts, runCommand, underline } from "@stacksjs/cli";
|
|
3
|
+
import {
|
|
4
|
+
addJumpBox,
|
|
5
|
+
deleteCdkRemnants,
|
|
6
|
+
deleteIamUsers,
|
|
7
|
+
deleteJumpBox,
|
|
8
|
+
deleteLogGroups,
|
|
9
|
+
deleteParameterStore,
|
|
10
|
+
deleteStacksBuckets,
|
|
11
|
+
deleteStacksFunctions,
|
|
12
|
+
deleteSubnets,
|
|
13
|
+
deleteVpcs,
|
|
14
|
+
getCloudFrontDistributionId,
|
|
15
|
+
getJumpBoxInstanceId
|
|
16
|
+
} from "@stacksjs/cloud";
|
|
17
|
+
import { path as p } from "@stacksjs/path";
|
|
18
|
+
import { ExitCode } from "@stacksjs/types";
|
|
19
|
+
async function createTemporaryCdkRole(roleName) {
|
|
20
|
+
const { AWSClient } = await import("@stacksjs/ts-cloud"), client = new AWSClient, trustPolicy = {
|
|
21
|
+
Version: "2012-10-17",
|
|
22
|
+
Statement: [
|
|
23
|
+
{
|
|
24
|
+
Effect: "Allow",
|
|
25
|
+
Principal: {
|
|
26
|
+
Service: "cloudformation.amazonaws.com"
|
|
27
|
+
},
|
|
28
|
+
Action: "sts:AssumeRole"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
try {
|
|
34
|
+
const getRoleParams = new URLSearchParams({
|
|
35
|
+
Action: "GetRole",
|
|
36
|
+
RoleName: roleName,
|
|
37
|
+
Version: "2010-05-08"
|
|
38
|
+
});
|
|
39
|
+
await client.request({
|
|
40
|
+
service: "iam",
|
|
41
|
+
region: "us-east-1",
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/",
|
|
44
|
+
body: getRoleParams.toString()
|
|
45
|
+
});
|
|
46
|
+
log.debug(`Role ${roleName} already exists`);
|
|
47
|
+
return;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (!e.message?.includes("NoSuchEntity") && !e.message?.includes("cannot be found"))
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
log.info("Creating temporary IAM role to enable stack deletion...");
|
|
53
|
+
const createRoleParams = new URLSearchParams({
|
|
54
|
+
Action: "CreateRole",
|
|
55
|
+
RoleName: roleName,
|
|
56
|
+
AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
|
|
57
|
+
Description: "Temporary role to allow CloudFormation to delete stuck stack",
|
|
58
|
+
Version: "2010-05-08"
|
|
59
|
+
});
|
|
60
|
+
await client.request({
|
|
61
|
+
service: "iam",
|
|
62
|
+
region: "us-east-1",
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/",
|
|
65
|
+
body: createRoleParams.toString()
|
|
66
|
+
});
|
|
67
|
+
log.success("Created IAM role");
|
|
68
|
+
log.info("Attaching permissions...");
|
|
69
|
+
const attachPolicyParams = new URLSearchParams({
|
|
70
|
+
Action: "AttachRolePolicy",
|
|
71
|
+
RoleName: roleName,
|
|
72
|
+
PolicyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
|
|
73
|
+
Version: "2010-05-08"
|
|
74
|
+
});
|
|
75
|
+
await client.request({
|
|
76
|
+
service: "iam",
|
|
77
|
+
region: "us-east-1",
|
|
78
|
+
method: "POST",
|
|
79
|
+
path: "/",
|
|
80
|
+
body: attachPolicyParams.toString()
|
|
81
|
+
});
|
|
82
|
+
log.success("IAM role ready for stack deletion");
|
|
83
|
+
log.info("Waiting for IAM role to propagate...");
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.message?.includes("EntityAlreadyExists"))
|
|
87
|
+
log.debug("Role already exists");
|
|
88
|
+
else
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function deleteTemporaryCdkRole(roleName) {
|
|
93
|
+
const { AWSClient } = await import("@stacksjs/ts-cloud"), client = new AWSClient;
|
|
94
|
+
try {
|
|
95
|
+
const detachPolicyParams = new URLSearchParams({
|
|
96
|
+
Action: "DetachRolePolicy",
|
|
97
|
+
RoleName: roleName,
|
|
98
|
+
PolicyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
|
|
99
|
+
Version: "2010-05-08"
|
|
100
|
+
});
|
|
101
|
+
await client.request({
|
|
102
|
+
service: "iam",
|
|
103
|
+
region: "us-east-1",
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/",
|
|
106
|
+
body: detachPolicyParams.toString()
|
|
107
|
+
});
|
|
108
|
+
const deleteRoleParams = new URLSearchParams({
|
|
109
|
+
Action: "DeleteRole",
|
|
110
|
+
RoleName: roleName,
|
|
111
|
+
Version: "2010-05-08"
|
|
112
|
+
});
|
|
113
|
+
await client.request({
|
|
114
|
+
service: "iam",
|
|
115
|
+
region: "us-east-1",
|
|
116
|
+
method: "POST",
|
|
117
|
+
path: "/",
|
|
118
|
+
body: deleteRoleParams.toString()
|
|
119
|
+
});
|
|
120
|
+
log.success("Cleaned up temporary IAM role");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
log.debug(`Could not clean up temporary role: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function isResultError(result) {
|
|
126
|
+
if (!result || typeof result !== "object")
|
|
127
|
+
return !1;
|
|
128
|
+
const r = result;
|
|
129
|
+
if (typeof r.isErr === "function")
|
|
130
|
+
return r.isErr();
|
|
131
|
+
return !!r.isErr;
|
|
132
|
+
}
|
|
133
|
+
function getResultError(result) {
|
|
134
|
+
if (!result || typeof result !== "object")
|
|
135
|
+
return "Unknown error";
|
|
136
|
+
return String(result.error || "Unknown error");
|
|
137
|
+
}
|
|
138
|
+
function getResultValue(result) {
|
|
139
|
+
if (!result || typeof result !== "object")
|
|
140
|
+
return;
|
|
141
|
+
return result.value;
|
|
142
|
+
}
|
|
143
|
+
export function cloud(buddy) {
|
|
144
|
+
const descriptions = {
|
|
145
|
+
cloud: "Interact with the Stacks Cloud",
|
|
146
|
+
ssh: "SSH into the Stacks Cloud",
|
|
147
|
+
add: "Add a resource to the Stacks Cloud",
|
|
148
|
+
remove: "Remove the Stacks Cloud. In case it fails, try again",
|
|
149
|
+
optimizeCost: "Remove certain resources that may be re-applied at a later time",
|
|
150
|
+
cleanUp: "Remove all resources that were retained during the cloud deletion",
|
|
151
|
+
invalidateCache: "Invalidate the CloudFront cache",
|
|
152
|
+
diff: "Show the diff of the current, undeployed cloud changes ",
|
|
153
|
+
dashboard: "Run the local Stacks Cloud management cockpit (servers, sites, deploys)",
|
|
154
|
+
host: "Host to bind the dashboard to",
|
|
155
|
+
port: "Port to bind the dashboard to",
|
|
156
|
+
env: "Environment to manage",
|
|
157
|
+
paths: "The paths to invalidate",
|
|
158
|
+
project: "Target a specific project",
|
|
159
|
+
verbose: "Enable verbose output"
|
|
160
|
+
};
|
|
161
|
+
buddy.command("cloud", descriptions.cloud).option("--ssh", descriptions.ssh, { default: !1 }).option("--connect", descriptions.ssh, { default: !1 }).option("--invalidate-cache", descriptions.invalidateCache, { default: !1 }).option("--paths [paths]", descriptions.paths).option("--diff", descriptions.diff, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
162
|
+
log.debug("Running `buddy cloud` ...", options);
|
|
163
|
+
const startTime = performance.now();
|
|
164
|
+
if (options.ssh || options.connect) {
|
|
165
|
+
const jumpBoxId = await getJumpBoxInstanceId(), result = await runCommand(`aws ssm start-session --target ${jumpBoxId}`, {
|
|
166
|
+
...options,
|
|
167
|
+
cwd: p.projectPath(),
|
|
168
|
+
stdin: "pipe"
|
|
169
|
+
});
|
|
170
|
+
if (isResultError(result)) {
|
|
171
|
+
await outro("While running the cloud command, there was an issue", { startTime, useSeconds: !0 }, getResultError(result));
|
|
172
|
+
process.exit(ExitCode.FatalError);
|
|
173
|
+
}
|
|
174
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
175
|
+
process.exit(ExitCode.Success);
|
|
176
|
+
}
|
|
177
|
+
if (options.invalidateCache) {
|
|
178
|
+
const { confirm } = await prompts({
|
|
179
|
+
name: "confirm",
|
|
180
|
+
type: "confirm",
|
|
181
|
+
message: "Would you like to invalidate the CDN (CloudFront) cache?"
|
|
182
|
+
});
|
|
183
|
+
if (!confirm) {
|
|
184
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
185
|
+
process.exit(ExitCode.Success);
|
|
186
|
+
}
|
|
187
|
+
log.info("Invalidating the CloudFront cache...");
|
|
188
|
+
const { AWSCloudFrontClient } = await import("@stacksjs/ts-cloud"), cloudfront = new AWSCloudFrontClient, distributionId = await getCloudFrontDistributionId();
|
|
189
|
+
try {
|
|
190
|
+
const invalidationId = await cloudfront.invalidateAll(distributionId);
|
|
191
|
+
log.success(`Invalidation created: ${invalidationId}`);
|
|
192
|
+
log.info("Status: pending");
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log.error(`Failed to invalidate CloudFront cache: ${err.message}`);
|
|
195
|
+
}
|
|
196
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
197
|
+
process.exit(ExitCode.Success);
|
|
198
|
+
}
|
|
199
|
+
if (options.diff) {
|
|
200
|
+
try {
|
|
201
|
+
const { InfrastructureGenerator } = await import("@stacksjs/ts-cloud"), { CloudFormationClient } = await import("@stacksjs/ts-cloud/aws"), { tsCloud: cloudConfig } = await import("~/config/cloud"), environment = process.env.APP_ENV || process.env.NODE_ENV || "production", newTemplate = new InfrastructureGenerator({
|
|
202
|
+
config: cloudConfig,
|
|
203
|
+
environment
|
|
204
|
+
}).generate().toJSON(), stackName = `${cloudConfig.project?.slug || "stacks"}-${environment}`, cfn = new CloudFormationClient(process.env.AWS_REGION || "us-east-1");
|
|
205
|
+
let currentTemplate = "{}";
|
|
206
|
+
try {
|
|
207
|
+
currentTemplate = (await cfn.getTemplate(stackName)).TemplateBody;
|
|
208
|
+
} catch {
|
|
209
|
+
log.info("No deployed stack found. Showing full template as diff.");
|
|
210
|
+
}
|
|
211
|
+
if (currentTemplate === newTemplate)
|
|
212
|
+
log.info("No changes detected.");
|
|
213
|
+
else {
|
|
214
|
+
log.info("Changes detected between deployed and local template:");
|
|
215
|
+
log.info(`Current template: ${currentTemplate.length} bytes`);
|
|
216
|
+
log.info(`New template: ${newTemplate.length} bytes`);
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
log.error(`Failed to compute diff: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
await outro("Cloud diff complete", { startTime, useSeconds: !0 });
|
|
222
|
+
process.exit(ExitCode.Success);
|
|
223
|
+
}
|
|
224
|
+
log.info("Not implemented yet. Read more about `buddy cloud` here: https://stacksjs.com/docs/cloud");
|
|
225
|
+
process.exit(ExitCode.Success);
|
|
226
|
+
});
|
|
227
|
+
buddy.command("cloud:add", descriptions.add).option("--jump-box", "Remove the jump-box", { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
228
|
+
log.debug("Running `buddy cloud:add` ...", options);
|
|
229
|
+
const startTime = await intro("buddy cloud:add");
|
|
230
|
+
if (options.jumpBox) {
|
|
231
|
+
const { confirm } = await prompts({
|
|
232
|
+
name: "confirm",
|
|
233
|
+
type: "confirm",
|
|
234
|
+
message: "Would you like to add a jump-box to your cloud?"
|
|
235
|
+
});
|
|
236
|
+
if (!confirm) {
|
|
237
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
238
|
+
process.exit(ExitCode.Success);
|
|
239
|
+
}
|
|
240
|
+
log.info("The jump-box is getting added to your cloud resources...");
|
|
241
|
+
log.info("This takes a few moments, please be patient.");
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
243
|
+
const result = await addJumpBox();
|
|
244
|
+
if (isResultError(result)) {
|
|
245
|
+
await outro("While running the cloud:add command, there was an issue", { startTime, useSeconds: !0 }, getResultError(result));
|
|
246
|
+
process.exit(ExitCode.FatalError);
|
|
247
|
+
}
|
|
248
|
+
log.info(italic("View the jump-box in the AWS console:"));
|
|
249
|
+
log.info(underline("https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#Instances:instanceState=running"));
|
|
250
|
+
log.info(italic("Once it finished initializing, you may SSH into it:"));
|
|
251
|
+
log.info(underline("buddy cloud --ssh"));
|
|
252
|
+
await outro("Your jump-box was added.", {
|
|
253
|
+
startTime,
|
|
254
|
+
useSeconds: !0
|
|
255
|
+
});
|
|
256
|
+
process.exit(ExitCode.Success);
|
|
257
|
+
}
|
|
258
|
+
log.info("This functionality is not yet implemented.");
|
|
259
|
+
process.exit(ExitCode.Success);
|
|
260
|
+
});
|
|
261
|
+
buddy.command("cloud:remove", descriptions.remove).alias("cloud:destroy").alias("cloud:rm").alias("undeploy").option("--jump-box", "Remove the jump-box", { default: !1 }).option("--force", "Force deletion of stack in bad state", { default: !1 }).option("--yes", "Skip confirmation prompts", { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
262
|
+
log.debug("Running `buddy cloud:remove` ...", options);
|
|
263
|
+
const startTime = await intro("buddy cloud:remove");
|
|
264
|
+
if (options.jumpBox) {
|
|
265
|
+
const { confirm } = await prompts({
|
|
266
|
+
name: "confirm",
|
|
267
|
+
type: "confirm",
|
|
268
|
+
message: "Would you like to remove your jump-box for now?"
|
|
269
|
+
});
|
|
270
|
+
if (!confirm) {
|
|
271
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
272
|
+
process.exit(ExitCode.Success);
|
|
273
|
+
}
|
|
274
|
+
const result = await deleteJumpBox();
|
|
275
|
+
if (isResultError(result)) {
|
|
276
|
+
await outro("While removing your jump-box, there was an issue", { startTime, useSeconds: !0 }, getResultError(result));
|
|
277
|
+
process.exit(ExitCode.FatalError);
|
|
278
|
+
}
|
|
279
|
+
await outro("Your jump-box was removed.", {
|
|
280
|
+
startTime,
|
|
281
|
+
useSeconds: !0
|
|
282
|
+
});
|
|
283
|
+
process.exit(ExitCode.Success);
|
|
284
|
+
}
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log("Removing cloud infrastructure...");
|
|
287
|
+
console.log(` ${italic("This typically takes 2-5 minutes.")}`);
|
|
288
|
+
console.log("");
|
|
289
|
+
const environment = process.env.APP_ENV || process.env.NODE_ENV || "production";
|
|
290
|
+
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
|
291
|
+
const { existsSync, readFileSync } = await import("node:fs"), { projectPath } = await import("@stacksjs/path"), envFiles = [
|
|
292
|
+
projectPath(`.env.${environment}`),
|
|
293
|
+
projectPath(".env")
|
|
294
|
+
];
|
|
295
|
+
for (const envPath of envFiles)
|
|
296
|
+
if (existsSync(envPath)) {
|
|
297
|
+
const lines = readFileSync(envPath, "utf-8").split(`
|
|
298
|
+
`);
|
|
299
|
+
for (const line of lines) {
|
|
300
|
+
const trimmed = line.trim();
|
|
301
|
+
if (trimmed.startsWith("#") || !trimmed.includes("="))
|
|
302
|
+
continue;
|
|
303
|
+
const [key, ...valueParts] = trimmed.split("="), value = valueParts.join("=").replace(/^["']|["']$/g, "");
|
|
304
|
+
if (key === "AWS_ACCESS_KEY_ID" || key === "AWS_SECRET_ACCESS_KEY" || key === "AWS_REGION" || key === "AWS_ACCOUNT_ID")
|
|
305
|
+
process.env[key] = value;
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
delete process.env.AWS_PROFILE;
|
|
311
|
+
try {
|
|
312
|
+
const { undeployStack } = await import("../../../actions/deploy"), region = process.env.AWS_REGION || "us-east-1";
|
|
313
|
+
await undeployStack({
|
|
314
|
+
environment,
|
|
315
|
+
region,
|
|
316
|
+
verbose: options.verbose
|
|
317
|
+
});
|
|
318
|
+
await outro("Cloud infrastructure removed", { startTime, useSeconds: !0 });
|
|
319
|
+
process.exit(ExitCode.Success);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.log("");
|
|
322
|
+
console.error("\u2717 Failed to remove cloud infrastructure");
|
|
323
|
+
const errorStr = String(error.message || error);
|
|
324
|
+
if (errorStr.includes("security token") || errorStr.includes("credentials")) {
|
|
325
|
+
console.log("");
|
|
326
|
+
console.error(" AWS credentials are invalid or expired");
|
|
327
|
+
console.log(" Check your AWS credentials in .env.production:");
|
|
328
|
+
console.log(" - AWS_ACCESS_KEY_ID");
|
|
329
|
+
console.log(" - AWS_SECRET_ACCESS_KEY");
|
|
330
|
+
} else if (errorStr.includes("region") || errorStr.includes("AWS_REGION")) {
|
|
331
|
+
console.log("");
|
|
332
|
+
console.error(" AWS Region not configured");
|
|
333
|
+
console.log(" Add AWS_REGION to your .env.production file");
|
|
334
|
+
} else if (errorStr.includes("AccessDenied")) {
|
|
335
|
+
console.log("");
|
|
336
|
+
console.error(" Access denied");
|
|
337
|
+
console.log(" Your AWS credentials may not have permission to delete stacks");
|
|
338
|
+
} else
|
|
339
|
+
console.error(` ${errorStr}`);
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log("Troubleshooting:");
|
|
342
|
+
console.log(" ./buddy cloud:cleanup - Clean up resources manually");
|
|
343
|
+
console.log(" --verbose - Show detailed error information");
|
|
344
|
+
console.log("");
|
|
345
|
+
if (options.verbose)
|
|
346
|
+
console.error("Error details:", error);
|
|
347
|
+
await outro("Failed to remove infrastructure", { startTime, useSeconds: !0 });
|
|
348
|
+
process.exit(ExitCode.FatalError);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
buddy.command("cloud:optimize-cost", descriptions.optimizeCost).option("--jump-box", "Remove the jump-box", { default: !0 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
352
|
+
log.debug("Running `buddy cloud:optimize-cost` ...", options);
|
|
353
|
+
const startTime = await intro("buddy cloud:optimize-cost");
|
|
354
|
+
if (options.jumpBox) {
|
|
355
|
+
const { confirm } = await prompts({
|
|
356
|
+
name: "confirm",
|
|
357
|
+
type: "confirm",
|
|
358
|
+
message: "Would you like to remove your jump-box to optimize your costs?"
|
|
359
|
+
});
|
|
360
|
+
if (!confirm) {
|
|
361
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
362
|
+
process.exit(ExitCode.Success);
|
|
363
|
+
}
|
|
364
|
+
await deleteJumpBox();
|
|
365
|
+
await outro("Your jump-box was removed & cost optimizations are applied.", { startTime, useSeconds: !0 });
|
|
366
|
+
process.exit(ExitCode.Success);
|
|
367
|
+
}
|
|
368
|
+
await outro("No cost optimization was applied", {
|
|
369
|
+
startTime,
|
|
370
|
+
useSeconds: !0
|
|
371
|
+
});
|
|
372
|
+
process.exit(ExitCode.Success);
|
|
373
|
+
});
|
|
374
|
+
buddy.command("cloud:cleanup", descriptions.cleanUp).alias("cloud:clean-up").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
375
|
+
log.debug("Running `buddy cloud:cleanup` ...", options);
|
|
376
|
+
const startTime = await intro("buddy cloud:cleanup");
|
|
377
|
+
delete process.env.AWS_PROFILE;
|
|
378
|
+
log.info("Cleaning up your cloud resources will take a while to complete. Please be patient.");
|
|
379
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
380
|
+
const cleanupSteps = [
|
|
381
|
+
{ label: "jump-boxes", fn: deleteJumpBox, ignoreErrors: ["Jump-box not found"] },
|
|
382
|
+
{ label: "retained S3 buckets", fn: deleteStacksBuckets },
|
|
383
|
+
{ label: "retained Lambda functions", fn: deleteStacksFunctions, ignoreErrors: ["No stacks functions found"] },
|
|
384
|
+
{ label: "remaining Stacks logs", fn: deleteLogGroups },
|
|
385
|
+
{ label: "stored parameters", fn: deleteParameterStore },
|
|
386
|
+
{ label: "VPCs", fn: deleteVpcs },
|
|
387
|
+
{ label: "Subnets", fn: deleteSubnets },
|
|
388
|
+
{ label: "CDK remnants", fn: deleteCdkRemnants },
|
|
389
|
+
{ label: "IAM users", fn: deleteIamUsers }
|
|
390
|
+
], errors = [];
|
|
391
|
+
for (const step of cleanupSteps) {
|
|
392
|
+
log.info(`Removing any ${step.label}...`);
|
|
393
|
+
try {
|
|
394
|
+
const result = await step.fn();
|
|
395
|
+
if (isResultError(result)) {
|
|
396
|
+
const errMsg = getResultError(result);
|
|
397
|
+
if (!step.ignoreErrors?.includes(errMsg)) {
|
|
398
|
+
log.warn(`${step.label} cleanup issue: ${errMsg}`);
|
|
399
|
+
errors.push({ label: step.label, error: errMsg });
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
const value = getResultValue(result);
|
|
403
|
+
if (value)
|
|
404
|
+
log.info(String(value));
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {
|
|
407
|
+
const errMsg = e.message || "AWS SDK error";
|
|
408
|
+
log.warn(`${step.label} cleanup skipped: ${errMsg}`);
|
|
409
|
+
errors.push({ label: step.label, error: errMsg });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (errors.length > 0) {
|
|
413
|
+
log.warn(`Cleanup completed with ${errors.length} issue(s):`);
|
|
414
|
+
for (const { label, error } of errors)
|
|
415
|
+
log.warn(` - ${label}: ${error}`);
|
|
416
|
+
}
|
|
417
|
+
await outro("AWS resources have been removed", {
|
|
418
|
+
startTime,
|
|
419
|
+
useSeconds: !0
|
|
420
|
+
});
|
|
421
|
+
process.exit(ExitCode.Success);
|
|
422
|
+
});
|
|
423
|
+
buddy.command("cloud:invalidate-cache", descriptions.invalidateCache).option("--paths [paths]", descriptions.paths, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
424
|
+
log.debug("Running `buddy cloud:invalidate-cache` ...", options);
|
|
425
|
+
const startTime = await intro("buddy cloud:invalidate-cache"), { confirm } = await prompts({
|
|
426
|
+
name: "confirm",
|
|
427
|
+
type: "confirm",
|
|
428
|
+
message: "Would you like to invalidate the CloudFront cache?"
|
|
429
|
+
});
|
|
430
|
+
if (!confirm) {
|
|
431
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
432
|
+
process.exit(ExitCode.Success);
|
|
433
|
+
}
|
|
434
|
+
log.info("Invalidating the CloudFront cache...");
|
|
435
|
+
const distributionId = await getCloudFrontDistributionId();
|
|
436
|
+
if (!distributionId) {
|
|
437
|
+
await outro("Could not resolve CloudFront distribution ID", { startTime, useSeconds: !0 }, "Ensure your cloud stack is deployed before invalidating cache.");
|
|
438
|
+
process.exit(ExitCode.FatalError);
|
|
439
|
+
}
|
|
440
|
+
const paths = options.paths ? String(options.paths) : "/*", result = await runCommand(`aws cloudfront create-invalidation --distribution-id ${distributionId} --paths ${paths}`, {
|
|
441
|
+
...options,
|
|
442
|
+
cwd: p.projectPath(),
|
|
443
|
+
stdin: "pipe"
|
|
444
|
+
});
|
|
445
|
+
if (isResultError(result)) {
|
|
446
|
+
await outro("While running the cloud command, there was an issue", { startTime, useSeconds: !0 }, getResultError(result));
|
|
447
|
+
process.exit(ExitCode.FatalError);
|
|
448
|
+
}
|
|
449
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
450
|
+
process.exit(ExitCode.Success);
|
|
451
|
+
});
|
|
452
|
+
buddy.command("cloud:diff", descriptions.diff).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
453
|
+
log.debug("Running `buddy cloud:diff` ...", options);
|
|
454
|
+
const startTime = await intro("buddy cloud:diff");
|
|
455
|
+
try {
|
|
456
|
+
const { InfrastructureGenerator } = await import("@stacksjs/ts-cloud"), { CloudFormationClient } = await import("@stacksjs/ts-cloud/aws"), { tsCloud: cloudConfig } = await import("~/config/cloud"), environment = process.env.APP_ENV || process.env.NODE_ENV || "production", newTemplate = new InfrastructureGenerator({
|
|
457
|
+
config: cloudConfig,
|
|
458
|
+
environment
|
|
459
|
+
}).generate().toJSON(), stackName = `${cloudConfig.project?.slug || "stacks"}-${environment}`, cfn = new CloudFormationClient(process.env.AWS_REGION || "us-east-1");
|
|
460
|
+
let currentTemplate = "{}";
|
|
461
|
+
try {
|
|
462
|
+
currentTemplate = (await cfn.getTemplate(stackName)).TemplateBody;
|
|
463
|
+
} catch {
|
|
464
|
+
log.info("No deployed stack found. Showing full template as diff.");
|
|
465
|
+
}
|
|
466
|
+
if (currentTemplate === newTemplate)
|
|
467
|
+
log.info("No changes detected.");
|
|
468
|
+
else {
|
|
469
|
+
log.info("Changes detected between deployed and local template:");
|
|
470
|
+
log.info(`Current template: ${currentTemplate.length} bytes`);
|
|
471
|
+
log.info(`New template: ${newTemplate.length} bytes`);
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
await outro("While running the cloud diff command, there was an issue", { startTime, useSeconds: !0 }, error.message);
|
|
475
|
+
process.exit(ExitCode.FatalError);
|
|
476
|
+
}
|
|
477
|
+
await outro("Cloud diff complete", { startTime, useSeconds: !0 });
|
|
478
|
+
process.exit(ExitCode.Success);
|
|
479
|
+
});
|
|
480
|
+
buddy.command("cloud:dashboard", descriptions.dashboard).alias("cloud:cockpit").option("--host [host]", descriptions.host, { default: "127.0.0.1" }).option("--port [port]", descriptions.port, { default: "7676" }).option("--env [env]", descriptions.env).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
481
|
+
log.debug("Running `buddy cloud:dashboard` ...", options);
|
|
482
|
+
const startTime = await intro("buddy cloud:dashboard"), tsCloud = await import("@stacksjs/ts-cloud");
|
|
483
|
+
if (typeof tsCloud.startLocalDashboardServer !== "function") {
|
|
484
|
+
await outro("The installed @stacksjs/ts-cloud does not provide the local cockpit yet", { startTime, useSeconds: !0 }, "Update your dependencies (requires @stacksjs/ts-cloud >= 0.5.27).");
|
|
485
|
+
process.exit(ExitCode.FatalError);
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const server = await tsCloud.startLocalDashboardServer({
|
|
489
|
+
host: options.host ? String(options.host) : void 0,
|
|
490
|
+
port: options.port ? Number(options.port) : void 0,
|
|
491
|
+
environment: options.env,
|
|
492
|
+
verbose: !!options.verbose
|
|
493
|
+
});
|
|
494
|
+
log.success(`Stacks Cloud cockpit running at ${underline(server.url)}`);
|
|
495
|
+
log.info(italic("Manage servers, sites, SSH keys and deploys. Press Ctrl+C to stop."));
|
|
496
|
+
await new Promise(() => {});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
await outro("While starting the cloud dashboard, there was an issue", { startTime, useSeconds: !0 }, error?.message ?? String(error));
|
|
499
|
+
process.exit(ExitCode.FatalError);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
onUnknownSubcommand(buddy, "cloud");
|
|
503
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { runCommit } from "@stacksjs/actions";
|
|
3
|
+
import { log } from "@stacksjs/logging";
|
|
4
|
+
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
5
|
+
export function commit(buddy) {
|
|
6
|
+
const descriptions = {
|
|
7
|
+
commit: "Commit your stashed changes",
|
|
8
|
+
project: "Target a specific project",
|
|
9
|
+
verbose: "Enable verbose output"
|
|
10
|
+
};
|
|
11
|
+
buddy.command("commit", descriptions.commit).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
12
|
+
log.debug("Running `buddy commit` ...", options);
|
|
13
|
+
await runCommit(options);
|
|
14
|
+
});
|
|
15
|
+
onUnknownSubcommand(buddy, "commit");
|
|
16
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { log, onUnknownSubcommand } from "@stacksjs/cli";
|
|
3
|
+
export function completion(buddy) {
|
|
4
|
+
const descriptions = {
|
|
5
|
+
completion: "Generate shell completion scripts",
|
|
6
|
+
shell: "Shell type (bash, zsh, fish)"
|
|
7
|
+
};
|
|
8
|
+
buddy.command("completion [shell]", descriptions.completion).option("-s, --shell [shell]", descriptions.shell).example("buddy completion bash").example("buddy completion zsh").example("buddy completion fish").example("buddy completion bash > /usr/local/etc/bash_completion.d/buddy").action(async (shell, options) => {
|
|
9
|
+
log.debug("Running `buddy completion` ...", options);
|
|
10
|
+
const targetShell = shell || options.shell || "bash", commandNames = (buddy.commands || []).map((cmd) => cmd.name).filter((name) => Boolean(name));
|
|
11
|
+
switch (targetShell) {
|
|
12
|
+
case "bash":
|
|
13
|
+
console.log(generateBashCompletion(commandNames));
|
|
14
|
+
break;
|
|
15
|
+
case "zsh":
|
|
16
|
+
console.log(generateZshCompletion(commandNames));
|
|
17
|
+
break;
|
|
18
|
+
case "fish":
|
|
19
|
+
console.log(generateFishCompletion(commandNames));
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
console.error(`Unknown shell: ${targetShell}`);
|
|
23
|
+
console.error("Supported shells: bash, zsh, fish");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
onUnknownSubcommand(buddy, "completion");
|
|
28
|
+
}
|
|
29
|
+
function generateBashCompletion(commands) {
|
|
30
|
+
return `# buddy completion for bash
|
|
31
|
+
|
|
32
|
+
_buddy_completion() {
|
|
33
|
+
local cur prev commands
|
|
34
|
+
COMPREPLY=()
|
|
35
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
36
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
37
|
+
|
|
38
|
+
commands="${commands.join(" ")}"
|
|
39
|
+
|
|
40
|
+
# Complete command names
|
|
41
|
+
if [ $COMP_CWORD -eq 1 ]; then
|
|
42
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Complete global flags
|
|
47
|
+
case "$prev" in
|
|
48
|
+
buddy|*)
|
|
49
|
+
COMPREPLY=( $(compgen -W "--help --version --verbose --quiet --debug --no-interaction --env --dry-run --force" -- "$cur") )
|
|
50
|
+
return 0
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
complete -F _buddy_completion buddy
|
|
56
|
+
|
|
57
|
+
# Installation:
|
|
58
|
+
# To install this completion script, save it to:
|
|
59
|
+
# - Linux: /etc/bash_completion.d/buddy or ~/.bash_completion
|
|
60
|
+
# - macOS: /usr/local/etc/bash_completion.d/buddy
|
|
61
|
+
#
|
|
62
|
+
# Then reload your shell or run: source ~/.bashrc
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
function generateZshCompletion(commands) {
|
|
66
|
+
return `#compdef buddy
|
|
67
|
+
|
|
68
|
+
# buddy completion for zsh
|
|
69
|
+
|
|
70
|
+
_buddy() {
|
|
71
|
+
local -a commands
|
|
72
|
+
commands=(
|
|
73
|
+
${commands.map((cmd) => ` '${cmd}:${cmd} command'`).join(`
|
|
74
|
+
`)}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
local -a global_flags
|
|
78
|
+
global_flags=(
|
|
79
|
+
'--help[Display help message]'
|
|
80
|
+
'--version[Display version number]'
|
|
81
|
+
'--verbose[Enable verbose output]'
|
|
82
|
+
'--quiet[Suppress non-essential output]'
|
|
83
|
+
'--debug[Enable debug mode]'
|
|
84
|
+
'--no-interaction[Do not ask interactive questions]'
|
|
85
|
+
'--env[Target environment]:environment:'
|
|
86
|
+
'--dry-run[Preview actions without executing]'
|
|
87
|
+
'--force[Skip confirmation prompts]'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
_arguments -C \\
|
|
91
|
+
'1: :->command' \\
|
|
92
|
+
'*::arg:->args' \\
|
|
93
|
+
$global_flags
|
|
94
|
+
|
|
95
|
+
case $state in
|
|
96
|
+
command)
|
|
97
|
+
_describe 'buddy command' commands
|
|
98
|
+
;;
|
|
99
|
+
*)
|
|
100
|
+
# Add subcommand-specific completions here if needed
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_buddy "$@"
|
|
106
|
+
|
|
107
|
+
# Installation:
|
|
108
|
+
# Save this file to a directory in your $fpath, for example:
|
|
109
|
+
# ~/.zsh/completions/_buddy
|
|
110
|
+
#
|
|
111
|
+
# Then add to your ~/.zshrc:
|
|
112
|
+
# fpath=(~/.zsh/completions $fpath)
|
|
113
|
+
# autoload -Uz compinit && compinit
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
function generateFishCompletion(commands) {
|
|
117
|
+
return `# buddy completion for fish
|
|
118
|
+
|
|
119
|
+
# Remove old completions
|
|
120
|
+
complete -c buddy -e
|
|
121
|
+
|
|
122
|
+
# Command completions
|
|
123
|
+
${commands.map((cmd) => `complete -c buddy -n "__fish_use_subcommand" -a "${cmd}" -d "${cmd} command"`).join(`
|
|
124
|
+
`)}
|
|
125
|
+
|
|
126
|
+
# Global flag completions
|
|
127
|
+
complete -c buddy -l help -d "Display help message"
|
|
128
|
+
complete -c buddy -l version -d "Display version number"
|
|
129
|
+
complete -c buddy -s v -l verbose -d "Enable verbose output"
|
|
130
|
+
complete -c buddy -s q -l quiet -d "Suppress non-essential output"
|
|
131
|
+
complete -c buddy -l debug -d "Enable debug mode"
|
|
132
|
+
complete -c buddy -s n -l no-interaction -d "Do not ask interactive questions"
|
|
133
|
+
complete -c buddy -l env -d "Target environment" -r
|
|
134
|
+
complete -c buddy -l dry-run -d "Preview actions without executing"
|
|
135
|
+
complete -c buddy -s f -l force -d "Skip confirmation prompts"
|
|
136
|
+
|
|
137
|
+
# Installation:
|
|
138
|
+
# Save this file to:
|
|
139
|
+
# ~/.config/fish/completions/buddy.fish
|
|
140
|
+
#
|
|
141
|
+
# Fish will automatically load it on startup
|
|
142
|
+
`;
|
|
143
|
+
}
|