@nmakarov/cli-toolkit 0.27.0 → 0.32.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.27.0",
3
+ "version": "0.32.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,8 @@
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"
72
81
  },
73
82
  "scripts": {
74
83
  "ssm:list": "node scripts/ssm/ssm-admin.js list",
@@ -146,6 +155,7 @@
146
155
  "files": [
147
156
  "dist/",
148
157
  "scripts/ssm/",
158
+ "scripts/deploy/",
149
159
  "README.md",
150
160
  "LICENSE"
151
161
  ],
@@ -159,8 +169,11 @@
159
169
  "vitest": "^4.0.3"
160
170
  },
161
171
  "dependencies": {
172
+ "@aws-sdk/client-ec2": "^3.1075.0",
173
+ "@aws-sdk/client-route-53": "^3.1075.0",
162
174
  "@aws-sdk/client-s3": "^3.750.0",
163
175
  "@aws-sdk/client-ssm": "^3.750.0",
176
+ "@aws-sdk/client-sts": "^3.1075.0",
164
177
  "axios": "^1.6.0",
165
178
  "chalk": "^4.1.2",
166
179
  "dotenv": "^17.2.3",
@@ -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);