@nmakarov/cli-toolkit 0.29.0 → 0.33.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmakarov/cli-toolkit",
3
- "version": "0.29.0",
3
+ "version": "0.33.0",
4
4
  "description": "A comprehensive toolkit for building CLI applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,6 +58,14 @@
58
58
  "import": "./dist/s3.js",
59
59
  "require": "./dist/s3.cjs"
60
60
  },
61
+ "./aws": {
62
+ "import": "./dist/aws.js",
63
+ "require": "./dist/aws.cjs"
64
+ },
65
+ "./deploy": {
66
+ "import": "./dist/deploy.js",
67
+ "require": "./dist/deploy.cjs"
68
+ },
61
69
  "./tasks": {
62
70
  "import": "./dist/tasks.js",
63
71
  "require": "./dist/tasks.cjs"
@@ -68,7 +76,9 @@
68
76
  }
69
77
  },
70
78
  "bin": {
71
- "cli-runner": "./dist/cli-runner.js"
79
+ "cli-runner": "./dist/cli-runner.js",
80
+ "cli-deploy": "./scripts/deploy/cli.js",
81
+ "cli-aws-discover": "./scripts/aws/discover.js"
72
82
  },
73
83
  "scripts": {
74
84
  "ssm:list": "node scripts/ssm/ssm-admin.js list",
@@ -146,6 +156,8 @@
146
156
  "files": [
147
157
  "dist/",
148
158
  "scripts/ssm/",
159
+ "scripts/deploy/",
160
+ "scripts/aws/",
149
161
  "README.md",
150
162
  "LICENSE"
151
163
  ],
@@ -159,8 +171,11 @@
159
171
  "vitest": "^4.0.3"
160
172
  },
161
173
  "dependencies": {
174
+ "@aws-sdk/client-ec2": "^3.1075.0",
175
+ "@aws-sdk/client-route-53": "^3.1075.0",
162
176
  "@aws-sdk/client-s3": "^3.750.0",
163
177
  "@aws-sdk/client-ssm": "^3.750.0",
178
+ "@aws-sdk/client-sts": "^3.1075.0",
164
179
  "axios": "^1.6.0",
165
180
  "chalk": "^4.1.2",
166
181
  "dotenv": "^17.2.3",
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli-aws-discover — read-only peek at the AWS account behind your credentials.
4
+ *
5
+ * Once @nmakarov/cli-toolkit is installed, run it without copying anything:
6
+ *
7
+ * npx cli-aws-discover
8
+ * npx cli-aws-discover --awsRegion=ca-central-1
9
+ *
10
+ * Credentials/region resolve from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY /
11
+ * AWS_REGION (env or .env), an AWS_PROFILE, or an instance role. If none are
12
+ * found — or AWS rejects them — it prints exactly how to get a key, no stack trace.
13
+ *
14
+ * Handy for finding the Route53 zone id / VPC / AMI you need for terraform.tfvars.
15
+ */
16
+
17
+ import { init } from "../../dist/init.js";
18
+ import { Aws } from "../../dist/aws.js";
19
+
20
+ const flow = async (context) => {
21
+ const { logger } = context;
22
+ logger.info("cli-aws-discover — read-only peek at your AWS account");
23
+
24
+ const aws = await Aws.init(context);
25
+
26
+ const cred = await aws.checkCredentials();
27
+ if (!cred.ok) {
28
+ logger.error(Aws.credentialsHelp(aws.getRegion()));
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ logger.info(`credentials: ${cred.source}`);
33
+
34
+ try {
35
+ const me = await aws.whoAmI();
36
+ logger.info(`account ${me.account} region ${aws.getRegion()}`);
37
+ logger.info(`identity ${me.arn}`);
38
+
39
+ const zones = await aws.listHostedZones();
40
+ logger.info(`\nRoute53 hosted zones (${zones.length}):`);
41
+ for (const z of zones) logger.info(` ${z.name.padEnd(30)} ${z.id}${z.private ? " [private]" : ""}`);
42
+
43
+ const vpcs = await aws.listVpcs();
44
+ logger.info(`\nVPCs (${vpcs.length}):`);
45
+ for (const v of vpcs) logger.info(` ${v.id} ${v.cidr}${v.isDefault ? " default" : ""} ${v.name ?? ""}`);
46
+
47
+ const ami = await aws.findLatestUbuntuAmi();
48
+ logger.info(`\nLatest Ubuntu AMI: ${ami ? `${ami.id} ${ami.name}` : "(none)"}`);
49
+ } catch (err) {
50
+ if (Aws.isAuthError(err)) {
51
+ logger.error(`AWS rejected the credentials it found (${err.name}).\n`);
52
+ logger.error(Aws.credentialsHelp(aws.getRegion()));
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ throw err;
57
+ }
58
+ };
59
+
60
+ init(flow);
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli-deploy — manifest-driven release deploys to EC2 (local or over SSH).
4
+ *
5
+ * Projects describe each app in a manifests module (default ./deploy/services.js)
6
+ * that exports `services` (a map/array of service manifests). Then:
7
+ *
8
+ * # from your laptop, over SSH (uses ~/.ssh/config Host alias):
9
+ * cli-deploy setup --service=web --host=web-prod
10
+ * cli-deploy deploy --service=web --host=web-prod
11
+ * cli-deploy rollback --service=web --host=web-prod
12
+ * cli-deploy status --service=web --host=web-prod
13
+ *
14
+ * # directly on the host (no --host):
15
+ * cli-deploy deploy --service=web
16
+ *
17
+ * Commands: setup | bootstrap | init | provision | deploy | rollback | status
18
+ *
19
+ * Common flags (cli-toolkit params; CLI/env/.env):
20
+ * --service (required) service name from the manifests module
21
+ * --manifests path to manifests module (default deploy/services.js)
22
+ * --host ssh Host alias → run remotely (omit to run locally)
23
+ * --appsRoot override the manifest appsRoot (handy for /tmp dry runs)
24
+ * --dryRun --skipPull --skipTests --skipNginx
25
+ * --withBootstrap --noDeploy (provision)
26
+ * --release=<stamp> (rollback to a specific release)
27
+ * --deployKey=~/.ssh/key (git deploy key, ssh repos)
28
+ * --envFile=path/to/.env (laptop .env to scp; default ./.env)
29
+ */
30
+
31
+ import { init } from "../../dist/init.js";
32
+ import {
33
+ loadServices,
34
+ resolveServiceFrom,
35
+ bootstrapHost,
36
+ initServiceStructure,
37
+ provisionService,
38
+ deployService,
39
+ rollbackService,
40
+ ensureRemoteRepo,
41
+ runRemoteCli,
42
+ runRemoteStatus,
43
+ servicePaths,
44
+ listReleases,
45
+ readCurrentRelease,
46
+ } from "../../dist/deploy.js";
47
+
48
+ const COMMANDS = ["setup", "bootstrap", "init", "provision", "deploy", "rollback", "status"];
49
+
50
+ function remotePassthrough(flags) {
51
+ const out = [];
52
+ const bools = ["dryRun", "skipPull", "skipTests", "skipNginx", "noDeploy", "withBootstrap"];
53
+ for (const k of bools) if (flags[k]) out.push(`--${k}`);
54
+ if (flags.release) out.push(`--release=${flags.release}`);
55
+ if (flags.deployKey) out.push(`--deployKey=${flags.deployKey}`);
56
+ return out;
57
+ }
58
+
59
+ async function localStatus(service, logger) {
60
+ const paths = servicePaths(service);
61
+ const current = await readCurrentRelease(paths);
62
+ const releases = await listReleases(paths);
63
+ logger.info(`service ${service.name}`);
64
+ logger.info(`appsRoot ${paths.root}`);
65
+ logger.info(`current ${current ?? "(not set)"}`);
66
+ logger.info(`releases ${releases.length ? releases.map((r) => r.name).join(", ") : "(none)"}`);
67
+ }
68
+
69
+ const flow = async (context) => {
70
+ const { logger, params } = context;
71
+ const command = (context.args.getCommands?.() ?? [])[0];
72
+ const serviceName = params.get("service", "string optional");
73
+ const host = params.get("host", "string optional");
74
+ const manifests = params.get("manifests", "string default deploy/services.js");
75
+
76
+ const flags = params.getAll({
77
+ dryRun: "boolean default false",
78
+ skipPull: "boolean default false",
79
+ skipTests: "boolean default false",
80
+ skipNginx: "boolean default false",
81
+ withBootstrap: "boolean default false",
82
+ noDeploy: "boolean default false",
83
+ appsRoot: "string optional",
84
+ release: "string optional",
85
+ deployKey: "string optional",
86
+ envFile: "string optional",
87
+ });
88
+
89
+ if (!command || !serviceName || !COMMANDS.includes(command)) {
90
+ logger.error("Usage: cli-deploy <command> --service=<name> [--host=<ssh>] [flags]");
91
+ logger.error(`Commands: ${COMMANDS.join(", ")}`);
92
+ logger.error("Example: cli-deploy deploy --service=web --host=web-prod");
93
+ process.exitCode = 2;
94
+ return;
95
+ }
96
+
97
+ let serviceMap;
98
+ try {
99
+ serviceMap = await loadServices({ manifests });
100
+ } catch (err) {
101
+ logger.error(err.message);
102
+ process.exitCode = 2;
103
+ return;
104
+ }
105
+
106
+ let service;
107
+ try {
108
+ service = resolveServiceFrom(serviceMap, serviceName, { appsRoot: flags.appsRoot });
109
+ } catch (err) {
110
+ logger.error(err.message);
111
+ process.exitCode = 2;
112
+ return;
113
+ }
114
+ if (flags.deployKey) service = { ...service, deployKey: flags.deployKey };
115
+
116
+ // ── Remote: orchestrate over SSH, re-invoking this CLI on the host ──────────
117
+ if (host) {
118
+ const extra = remotePassthrough(flags);
119
+ const opts = { logger, manifests, deployKey: service.deployKey, envFile: flags.envFile };
120
+ logger.info(`remote ${command} service=${service.name} host=${host}`);
121
+
122
+ switch (command) {
123
+ case "setup":
124
+ await ensureRemoteRepo(host, service, opts);
125
+ await runRemoteCli(host, service, "bootstrap", extra, { ...opts, skipPull: true });
126
+ await runRemoteCli(host, service, "init", extra, { ...opts, skipPull: true });
127
+ await runRemoteCli(host, service, "provision", extra, { ...opts, skipPull: true });
128
+ break;
129
+ case "status":
130
+ await runRemoteStatus(host, service, { logger });
131
+ break;
132
+ default:
133
+ await runRemoteCli(host, service, command, extra, opts);
134
+ }
135
+ return;
136
+ }
137
+
138
+ // ── Local: run the step(s) on this machine (the host, or a dry run) ─────────
139
+ logger.info(`local ${command} service=${service.name}`);
140
+ switch (command) {
141
+ case "bootstrap":
142
+ await bootstrapHost(service, { deployKey: service.deployKey, dryRun: flags.dryRun, logger });
143
+ break;
144
+ case "init":
145
+ await initServiceStructure(service, { dryRun: flags.dryRun, logger });
146
+ break;
147
+ case "provision":
148
+ await provisionService(service, {
149
+ dryRun: flags.dryRun,
150
+ deploy: !flags.noDeploy,
151
+ skipBootstrap: !flags.withBootstrap,
152
+ deployKey: flags.withBootstrap ? service.deployKey : undefined,
153
+ logger,
154
+ });
155
+ break;
156
+ case "deploy":
157
+ await deployService(service, {
158
+ dryRun: flags.dryRun,
159
+ skipPull: flags.skipPull,
160
+ skipTests: flags.skipTests,
161
+ skipNginx: flags.skipNginx,
162
+ logger,
163
+ });
164
+ break;
165
+ case "rollback":
166
+ await rollbackService(service, { release: flags.release, dryRun: flags.dryRun, logger });
167
+ break;
168
+ case "setup":
169
+ await bootstrapHost(service, { deployKey: service.deployKey, dryRun: flags.dryRun, logger });
170
+ await initServiceStructure(service, { dryRun: flags.dryRun, logger });
171
+ await provisionService(service, { dryRun: flags.dryRun, deploy: !flags.noDeploy, skipBootstrap: true, logger });
172
+ break;
173
+ case "status":
174
+ await localStatus(service, logger);
175
+ break;
176
+ default:
177
+ break;
178
+ }
179
+ };
180
+
181
+ init(flow);