slskit-cli 1.0.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/CHANGELOG.md +61 -0
- package/COMMAND.md +636 -0
- package/LICENSE +21 -0
- package/NOTICE.md +22 -0
- package/README.md +168 -0
- package/bin/slskit.js +3 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +25 -0
- package/dist/commands/base-command.d.ts +5 -0
- package/dist/commands/base-command.js +29 -0
- package/dist/commands/configure/action.d.ts +2 -0
- package/dist/commands/configure/action.js +120 -0
- package/dist/commands/configure/aws-cli.d.ts +16 -0
- package/dist/commands/configure/aws-cli.js +135 -0
- package/dist/commands/configure/index.d.ts +2 -0
- package/dist/commands/configure/index.js +20 -0
- package/dist/commands/configure/prompts.d.ts +8 -0
- package/dist/commands/configure/prompts.js +117 -0
- package/dist/commands/configure/types.d.ts +24 -0
- package/dist/commands/configure/types.js +6 -0
- package/dist/commands/deploy/action.d.ts +2 -0
- package/dist/commands/deploy/action.js +115 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +23 -0
- package/dist/commands/deploy/sam-deploy.d.ts +10 -0
- package/dist/commands/deploy/sam-deploy.js +127 -0
- package/dist/commands/deploy/scope.d.ts +9 -0
- package/dist/commands/deploy/scope.js +34 -0
- package/dist/commands/deploy/types.d.ts +20 -0
- package/dist/commands/deploy/types.js +2 -0
- package/dist/commands/env/action.d.ts +11 -0
- package/dist/commands/env/action.js +274 -0
- package/dist/commands/env/dotenv.d.ts +6 -0
- package/dist/commands/env/dotenv.js +99 -0
- package/dist/commands/env/index.d.ts +2 -0
- package/dist/commands/env/index.js +76 -0
- package/dist/commands/env/keys.d.ts +8 -0
- package/dist/commands/env/keys.js +41 -0
- package/dist/commands/env/parameters.d.ts +7 -0
- package/dist/commands/env/parameters.js +63 -0
- package/dist/commands/env/templates.d.ts +3 -0
- package/dist/commands/env/templates.js +65 -0
- package/dist/commands/env/types.d.ts +21 -0
- package/dist/commands/env/types.js +2 -0
- package/dist/commands/function/action.d.ts +2 -0
- package/dist/commands/function/action.js +71 -0
- package/dist/commands/function/generator.d.ts +8 -0
- package/dist/commands/function/generator.js +58 -0
- package/dist/commands/function/index.d.ts +2 -0
- package/dist/commands/function/index.js +26 -0
- package/dist/commands/function/manifest.d.ts +5 -0
- package/dist/commands/function/manifest.js +57 -0
- package/dist/commands/function/prompts.d.ts +18 -0
- package/dist/commands/function/prompts.js +196 -0
- package/dist/commands/function/tooling.d.ts +2 -0
- package/dist/commands/function/tooling.js +43 -0
- package/dist/commands/function/types.d.ts +52 -0
- package/dist/commands/function/types.js +2 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/init/action.d.ts +2 -0
- package/dist/commands/init/action.js +36 -0
- package/dist/commands/init/content.d.ts +2 -0
- package/dist/commands/init/content.js +97 -0
- package/dist/commands/init/index.d.ts +2 -0
- package/dist/commands/init/index.js +30 -0
- package/dist/commands/init/manifest.d.ts +6 -0
- package/dist/commands/init/manifest.js +147 -0
- package/dist/commands/init/prompts.d.ts +6 -0
- package/dist/commands/init/prompts.js +155 -0
- package/dist/commands/init/scaffold.d.ts +7 -0
- package/dist/commands/init/scaffold.js +85 -0
- package/dist/commands/init/templates/helpers.d.ts +9 -0
- package/dist/commands/init/templates/helpers.js +42 -0
- package/dist/commands/init/templates/node.d.ts +4 -0
- package/dist/commands/init/templates/node.js +99 -0
- package/dist/commands/init/templates/project.d.ts +8 -0
- package/dist/commands/init/templates/project.js +239 -0
- package/dist/commands/init/templates/python.d.ts +4 -0
- package/dist/commands/init/templates/python.js +65 -0
- package/dist/commands/init/templates/sam.d.ts +4 -0
- package/dist/commands/init/templates/sam.js +369 -0
- package/dist/commands/init/templates/shared-code.d.ts +6 -0
- package/dist/commands/init/templates/shared-code.js +159 -0
- package/dist/commands/init/types.d.ts +47 -0
- package/dist/commands/init/types.js +75 -0
- package/dist/commands/rm/action.d.ts +2 -0
- package/dist/commands/rm/action.js +89 -0
- package/dist/commands/rm/index.d.ts +2 -0
- package/dist/commands/rm/index.js +25 -0
- package/dist/commands/rm/plan.d.ts +19 -0
- package/dist/commands/rm/plan.js +78 -0
- package/dist/commands/rm/prompts.d.ts +4 -0
- package/dist/commands/rm/prompts.js +107 -0
- package/dist/commands/rm/types.d.ts +7 -0
- package/dist/commands/rm/types.js +2 -0
- package/dist/commands/run/action.d.ts +2 -0
- package/dist/commands/run/action.js +171 -0
- package/dist/commands/run/index.d.ts +2 -0
- package/dist/commands/run/index.js +21 -0
- package/dist/commands/run/local-template.d.ts +7 -0
- package/dist/commands/run/local-template.js +51 -0
- package/dist/commands/run/manifest.d.ts +2 -0
- package/dist/commands/run/manifest.js +20 -0
- package/dist/commands/run/resources.d.ts +20 -0
- package/dist/commands/run/resources.js +93 -0
- package/dist/commands/run/sam-cli.d.ts +10 -0
- package/dist/commands/run/sam-cli.js +183 -0
- package/dist/commands/run/types.d.ts +18 -0
- package/dist/commands/run/types.js +2 -0
- package/dist/commands/run/watch.d.ts +11 -0
- package/dist/commands/run/watch.js +131 -0
- package/dist/core/context.d.ts +15 -0
- package/dist/core/context.js +41 -0
- package/dist/core/credentials.d.ts +10 -0
- package/dist/core/credentials.js +22 -0
- package/dist/core/environments.d.ts +46 -0
- package/dist/core/environments.js +161 -0
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/flags-docs.d.ts +2 -0
- package/dist/core/flags-docs.js +63 -0
- package/dist/core/is-ci.d.ts +4 -0
- package/dist/core/is-ci.js +16 -0
- package/dist/core/logger.d.ts +4 -0
- package/dist/core/logger.js +19 -0
- package/dist/core/run.d.ts +1 -0
- package/dist/core/run.js +27 -0
- package/dist/core/scope.d.ts +33 -0
- package/dist/core/scope.js +108 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +26 -0
- package/package.json +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Savinda Jayasekara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# NOTICE
|
|
2
|
+
|
|
3
|
+
slskit is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 Savinda Jayasekara.
|
|
6
|
+
|
|
7
|
+
## Third-party runtime software
|
|
8
|
+
|
|
9
|
+
Production dependencies and their licenses (also checked by `npm run licenses`):
|
|
10
|
+
|
|
11
|
+
| Package | License |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| commander | MIT |
|
|
14
|
+
| @inquirer/prompts | MIT |
|
|
15
|
+
|
|
16
|
+
Each of those packages includes its own copyright and license notice. Those
|
|
17
|
+
notices remain in force.
|
|
18
|
+
|
|
19
|
+
## Reference material
|
|
20
|
+
|
|
21
|
+
Architecture and CI practices were informed by publicly documented patterns from
|
|
22
|
+
the Netlify CLI (MIT). No Netlify source code is included in this repository.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# slskit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/savindaJ/SLSKIT-CLI/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/slskit-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/slskit-cli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A CLI for scaffolding and growing multi-service AWS Lambda projects on AWS SAM.
|
|
9
|
+
|
|
10
|
+
Starting a serverless project usually means a day of wiring: a template, a build
|
|
11
|
+
setup, handlers, an API Gateway, environment variables that behave the same locally
|
|
12
|
+
and in AWS, and a deploy that doesn't fight you. `slskit` gives you all of it in one
|
|
13
|
+
command, then stays useful — adding functions, managing environments, running the
|
|
14
|
+
project locally, and deploying a single function when that's all that changed.
|
|
15
|
+
|
|
16
|
+
Two runtime dependencies. Everything that touches AWS goes through the official
|
|
17
|
+
`sam` and `aws` CLIs.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g slskit-cli
|
|
21
|
+
slskit init my-lambda-app
|
|
22
|
+
cd my-lambda-app
|
|
23
|
+
slskit run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
In CI, install it locally and pin the version with a lockfile. A global install
|
|
27
|
+
always pulls the latest release, including breaking changes.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev slskit-cli
|
|
31
|
+
npx slskit deploy --yes
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's a running API on `http://127.0.0.1:3000` with two services, four functions, a
|
|
35
|
+
shared Lambda layer, and hot reload on save.
|
|
36
|
+
|
|
37
|
+
## What you get
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
my-lambda-app/
|
|
41
|
+
├── src/
|
|
42
|
+
│ ├── functions/ # handlers, one folder per service
|
|
43
|
+
│ ├── services/ # business logic the handlers import
|
|
44
|
+
│ └── shared/ # shared utilities, published as a Lambda layer
|
|
45
|
+
├── templates/ # one CloudFormation stack per service
|
|
46
|
+
├── template.yaml # root stack, nests them
|
|
47
|
+
├── .env.dev # the dev environment's variables (gitignored)
|
|
48
|
+
└── slskit.json # the project graph every command reads
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A root stack nesting one template per service, so the root file stays short as the
|
|
52
|
+
project grows and services can be deployed one at a time. TypeScript, JavaScript or
|
|
53
|
+
Python — and individual functions can differ from the rest of the project.
|
|
54
|
+
|
|
55
|
+
## Highlights
|
|
56
|
+
|
|
57
|
+
**Environments that don't leak into each other.** Every environment is its own deploy
|
|
58
|
+
target: region, profile, CloudFormation stack, variables. `APP_ENVIRONMENT` is
|
|
59
|
+
generated for each one, and every resource name is built from it — `shop-dev-login`
|
|
60
|
+
and `shop-production-login` are different functions in different stacks.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
slskit env add production --profile prod-admin --region eu-west-2
|
|
64
|
+
slskit env set API_KEY=sk-live-xxx --secret --env production
|
|
65
|
+
slskit deploy production
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Variables that reach your code the same way locally and in AWS.** Add one with
|
|
69
|
+
`slskit env set`, or just type it into `.env.<environment>` — either way it becomes a
|
|
70
|
+
CloudFormation parameter wired into every function, and `process.env.YOUR_KEY` works
|
|
71
|
+
in both places. Secrets stay out of `slskit.json`, and `--ssm` keeps them out of the
|
|
72
|
+
CLI entirely.
|
|
73
|
+
|
|
74
|
+
**Run or deploy just the part you're working on.**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
slskit run --service auth # only auth's routes, so only those build
|
|
78
|
+
slskit deploy dev --function login # code-only update of one function
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both commands ask if you don't tell them. A scoped run builds only the functions it
|
|
82
|
+
serves; a scoped deploy uses `sam sync --code`, which is far faster than a full one.
|
|
83
|
+
|
|
84
|
+
**Hot reload.** `slskit run` watches `src/` and rebuilds only the function you
|
|
85
|
+
touched — the next request uses the new code, no restart. A broken file leaves the
|
|
86
|
+
previous build serving and waits for the next save.
|
|
87
|
+
|
|
88
|
+
**A deploy that tells you what it will do.** It stops before calling AWS if the
|
|
89
|
+
region is missing, the credentials don't verify, or a declared secret has no value —
|
|
90
|
+
so a failed deploy costs nothing. Then it prints the plan, asks, and reports the API
|
|
91
|
+
Gateway URLs when it's done.
|
|
92
|
+
|
|
93
|
+
**Credentials never enter your project.** `slskit configure` stores only the AWS
|
|
94
|
+
profile *name* in `slskit.json`; access keys go to `~/.aws/credentials` via the AWS
|
|
95
|
+
CLI. Secrets are never echoed, and override values are never logged.
|
|
96
|
+
|
|
97
|
+
## Commands
|
|
98
|
+
|
|
99
|
+
| Command | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `slskit init [name]` | Scaffold a new project |
|
|
102
|
+
| `slskit run [environment]` | Run it locally on one API Gateway port |
|
|
103
|
+
| `slskit function [name]` | Add a function to a new or existing service |
|
|
104
|
+
| `slskit rm [name]` | Remove a function or a service, and everything that referenced it |
|
|
105
|
+
| `slskit configure` | Set AWS credentials and the deploy target |
|
|
106
|
+
| `slskit env` | Manage environments and their variables |
|
|
107
|
+
| `slskit deploy [environment]` | Deploy to AWS |
|
|
108
|
+
|
|
109
|
+
**[COMMAND.md](COMMAND.md) is the full reference** — every flag, every behaviour,
|
|
110
|
+
and the reasoning behind the parts that aren't obvious.
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
| | |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Node.js | 18 or newer |
|
|
117
|
+
| [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) | to run and deploy — `slskit run` offers to install it |
|
|
118
|
+
| [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) | to verify credentials and read stack outputs |
|
|
119
|
+
| Docker | for `sam local`, which runs functions in Lambda-like containers |
|
|
120
|
+
|
|
121
|
+
## First deploy
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
slskit configure --set-credentials # asks for a key, writes ~/.aws/credentials
|
|
125
|
+
slskit deploy dev
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
API Gateway, the functions, the layer and any tables are created in one pass —
|
|
129
|
+
`--resolve-s3` provisions the artifact bucket, so there's no manual setup. It prints
|
|
130
|
+
the URLs at the end:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
Deployed the whole project of "shop" to "dev".
|
|
134
|
+
stack: shop-dev
|
|
135
|
+
region: us-east-1
|
|
136
|
+
|
|
137
|
+
Endpoints:
|
|
138
|
+
ApiUrl: https://6hjgplpky0.execute-api.us-east-1.amazonaws.com
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
git clone https://github.com/savindaJ/SLSKIT-CLI.git
|
|
145
|
+
cd SLSKIT-CLI
|
|
146
|
+
npm install
|
|
147
|
+
npm test
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm run build && npm link # use your working copy as the real slskit command
|
|
152
|
+
npm run dev # run from source via tsx
|
|
153
|
+
npm run typecheck # tsc --noEmit
|
|
154
|
+
npm run docs # regenerate docs/flags.md from Commander
|
|
155
|
+
npm run licenses # check the project and production dependency licenses
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Bug reports and
|
|
159
|
+
documentation fixes are just as welcome as code.
|
|
160
|
+
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
Report vulnerabilities privately — see [SECURITY.md](SECURITY.md), which also
|
|
164
|
+
documents exactly how slskit handles your credentials.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
[MIT](LICENSE) © Savinda Jayasekara. Third-party notices are in [NOTICE.md](NOTICE.md).
|
package/bin/slskit.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const errors_js_1 = require("./core/errors.js");
|
|
4
|
+
const logger_js_1 = require("./core/logger.js");
|
|
5
|
+
const program_js_1 = require("./program.js");
|
|
6
|
+
async function main() {
|
|
7
|
+
const program = (0, program_js_1.createProgram)();
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
9
|
+
if (argv.length === 0) {
|
|
10
|
+
program.outputHelp();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await program.parseAsync(process.argv);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof errors_js_1.CliError) {
|
|
18
|
+
logger_js_1.logger.error(error.message);
|
|
19
|
+
process.exitCode = error.exitCode;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
void main();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseCommand = void 0;
|
|
4
|
+
exports.addGlobalOptions = addGlobalOptions;
|
|
5
|
+
const commander_1 = require("commander");
|
|
6
|
+
const context_js_1 = require("../core/context.js");
|
|
7
|
+
function addGlobalOptions(command) {
|
|
8
|
+
return command
|
|
9
|
+
.addOption(new commander_1.Option("--cwd <path>", "Run as if started from this directory"))
|
|
10
|
+
.addOption(new commander_1.Option("--debug", "Print stack traces when a command fails"))
|
|
11
|
+
.addOption(new commander_1.Option("--silent", "Silence informational output"))
|
|
12
|
+
.addOption(new commander_1.Option("--json", "JSON output where a command supports it"));
|
|
13
|
+
}
|
|
14
|
+
function attachGlobalHook(command) {
|
|
15
|
+
command.hook("preAction", (_parent, actionCommand) => {
|
|
16
|
+
(0, context_js_1.applyGlobalOptions)(actionCommand.optsWithGlobals());
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Every command created through program.command() inherits the global flags,
|
|
20
|
+
// so `slskit deploy --debug` and `slskit --debug deploy` both work.
|
|
21
|
+
class BaseCommand extends commander_1.Command {
|
|
22
|
+
createCommand(name) {
|
|
23
|
+
const command = new BaseCommand(name);
|
|
24
|
+
addGlobalOptions(command);
|
|
25
|
+
attachGlobalHook(command);
|
|
26
|
+
return command;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.BaseCommand = BaseCommand;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureAction = configureAction;
|
|
4
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
5
|
+
const logger_js_1 = require("../../core/logger.js");
|
|
6
|
+
const environments_js_1 = require("../../core/environments.js");
|
|
7
|
+
const action_js_1 = require("../env/action.js");
|
|
8
|
+
const aws_cli_js_1 = require("./aws-cli.js");
|
|
9
|
+
const prompts_js_1 = require("./prompts.js");
|
|
10
|
+
const ACCESS_KEY_PATTERN = /^[A-Z0-9]{16,128}$/;
|
|
11
|
+
function credentialSource(profile) {
|
|
12
|
+
return profile ? `profile "${profile}"` : "the environment";
|
|
13
|
+
}
|
|
14
|
+
function fixHint(profile) {
|
|
15
|
+
return profile
|
|
16
|
+
? `aws configure --profile ${profile} (or "aws sso login --profile ${profile}" for an SSO profile)`
|
|
17
|
+
: "Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or pick a named profile instead.";
|
|
18
|
+
}
|
|
19
|
+
// Prompts for an access key and hands it to the AWS CLI, which stores it in
|
|
20
|
+
// ~/.aws/credentials. The secret is never echoed, never logged, and never written
|
|
21
|
+
// anywhere inside the project.
|
|
22
|
+
async function promptForCredentials(answers) {
|
|
23
|
+
const { input, password, confirm } = await import("@inquirer/prompts");
|
|
24
|
+
const proceed = await confirm({
|
|
25
|
+
message: "Enter an AWS access key now and store it in ~/.aws/credentials?",
|
|
26
|
+
default: true,
|
|
27
|
+
});
|
|
28
|
+
if (!proceed) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const profile = answers.profile ??
|
|
32
|
+
(await input({
|
|
33
|
+
message: "Profile name to store it under:",
|
|
34
|
+
default: "default",
|
|
35
|
+
required: true,
|
|
36
|
+
}));
|
|
37
|
+
const accessKeyId = (await input({
|
|
38
|
+
message: "AWS Access Key ID:",
|
|
39
|
+
required: true,
|
|
40
|
+
})).trim();
|
|
41
|
+
if (!ACCESS_KEY_PATTERN.test(accessKeyId)) {
|
|
42
|
+
throw new errors_js_1.CliError(`"${accessKeyId}" does not look like an AWS Access Key ID (expected something like AKIA... ).`);
|
|
43
|
+
}
|
|
44
|
+
const secretAccessKey = (await password({ message: "AWS Secret Access Key:", mask: true })).trim();
|
|
45
|
+
if (!secretAccessKey) {
|
|
46
|
+
throw new errors_js_1.CliError("A secret access key is required.");
|
|
47
|
+
}
|
|
48
|
+
const { ok, error } = (0, aws_cli_js_1.setProfileCredentials)(profile, accessKeyId, secretAccessKey, answers.region);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
throw new errors_js_1.CliError(`Could not store the credentials: ${error ?? "aws configure set failed."}\nStore them yourself with: aws configure --profile ${profile}`);
|
|
51
|
+
}
|
|
52
|
+
logger_js_1.logger.info(`\nStored the key for profile "${profile}" in ${(0, aws_cli_js_1.credentialsFilePath)()}.`);
|
|
53
|
+
logger_js_1.logger.info(" the secret was not echoed and is not stored in this project");
|
|
54
|
+
return profile;
|
|
55
|
+
}
|
|
56
|
+
// Returns the profile actually used, which may change if credentials were entered
|
|
57
|
+
// against a new profile name.
|
|
58
|
+
async function verifyCredentials(answers, options) {
|
|
59
|
+
if (options.skipVerify) {
|
|
60
|
+
logger_js_1.logger.info("\nSkipped the credential check (--skip-verify).");
|
|
61
|
+
return { profile: answers.profile };
|
|
62
|
+
}
|
|
63
|
+
if (!(0, aws_cli_js_1.isAwsCliAvailable)()) {
|
|
64
|
+
logger_js_1.logger.info(`\nAWS CLI was not found on your PATH, so the credentials were not verified.\n Install it: ${aws_cli_js_1.AWS_INSTALL_URL}\n Then re-run "slskit configure" to check them.`);
|
|
65
|
+
return { profile: answers.profile };
|
|
66
|
+
}
|
|
67
|
+
let profile = answers.profile;
|
|
68
|
+
let { identity, error } = (0, aws_cli_js_1.getCallerIdentity)(profile, answers.region);
|
|
69
|
+
const wantsNewKey = options.setCredentials && process.stdin.isTTY;
|
|
70
|
+
if (!identity || wantsNewKey) {
|
|
71
|
+
if (!process.stdin.isTTY) {
|
|
72
|
+
throw new errors_js_1.CliError(`Could not verify AWS credentials for ${credentialSource(profile)} in ${answers.region}.\n ${error ?? "sts get-caller-identity failed."}\n\nFix them with:\n ${fixHint(profile)}\n\nThen re-run "slskit configure", or pass --skip-verify to save without checking.`);
|
|
73
|
+
}
|
|
74
|
+
if (!identity) {
|
|
75
|
+
logger_js_1.logger.info(`\nCould not verify AWS credentials for ${credentialSource(profile)} in ${answers.region}.`);
|
|
76
|
+
logger_js_1.logger.info(` ${error ?? "sts get-caller-identity failed."}`);
|
|
77
|
+
}
|
|
78
|
+
const stored = await promptForCredentials(answers);
|
|
79
|
+
if (!stored) {
|
|
80
|
+
throw new errors_js_1.CliError(`AWS credentials are required to deploy.\n\nFix them with:\n ${fixHint(profile)}\n\nThen re-run "slskit configure", or pass --skip-verify to save without checking.`);
|
|
81
|
+
}
|
|
82
|
+
profile = stored;
|
|
83
|
+
({ identity, error } = (0, aws_cli_js_1.getCallerIdentity)(profile, answers.region));
|
|
84
|
+
if (!identity) {
|
|
85
|
+
throw new errors_js_1.CliError(`The key was stored for profile "${profile}", but AWS still rejected it in ${answers.region}.\n ${error ?? "sts get-caller-identity failed."}\n\nCheck that the key is active and has permission to call sts:GetCallerIdentity.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
logger_js_1.logger.info("\nVerified AWS credentials.");
|
|
89
|
+
logger_js_1.logger.info(` account: ${identity.account}`);
|
|
90
|
+
logger_js_1.logger.info(` identity: ${identity.arn}`);
|
|
91
|
+
return { profile, identity };
|
|
92
|
+
}
|
|
93
|
+
async function configureAction(options) {
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const manifest = (0, environments_js_1.readManifest)(cwd, "slskit configure");
|
|
96
|
+
const answers = await (0, prompts_js_1.collectConfigureAnswers)(manifest, options);
|
|
97
|
+
const { profile } = await verifyCredentials(answers, options);
|
|
98
|
+
const existing = manifest.environments?.list?.[answers.environment];
|
|
99
|
+
const config = {
|
|
100
|
+
region: answers.region,
|
|
101
|
+
stackName: answers.stackName,
|
|
102
|
+
};
|
|
103
|
+
if (profile) {
|
|
104
|
+
config.profile = profile;
|
|
105
|
+
}
|
|
106
|
+
// Variables already recorded for this environment survive a re-configure.
|
|
107
|
+
if (existing?.variables) {
|
|
108
|
+
config.variables = existing.variables;
|
|
109
|
+
}
|
|
110
|
+
(0, environments_js_1.writeEnvironment)(cwd, manifest, answers.environment, config);
|
|
111
|
+
const dotenv = (0, action_js_1.seedDotenv)(cwd, answers.environment);
|
|
112
|
+
logger_js_1.logger.info(`\nConfigured environment "${answers.environment}" for "${manifest.name}".`);
|
|
113
|
+
logger_js_1.logger.info(` ${environments_js_1.APP_ENVIRONMENT_KEY}: ${answers.environment}`);
|
|
114
|
+
logger_js_1.logger.info(` credentials: ${credentialSource(profile)}`);
|
|
115
|
+
logger_js_1.logger.info(` region: ${answers.region}`);
|
|
116
|
+
logger_js_1.logger.info(` stack: ${answers.stackName}`);
|
|
117
|
+
logger_js_1.logger.info(` written to: ${environments_js_1.MANIFEST_FILE}, ${dotenv}`);
|
|
118
|
+
logger_js_1.logger.info("\nNo credential material is stored in this project — only the profile name, resolved at deploy time.");
|
|
119
|
+
logger_js_1.logger.info(`\nDeploy it with: slskit deploy ${answers.environment}`);
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IdentityResult } from "./types.js";
|
|
2
|
+
export declare const AWS_INSTALL_URL = "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html";
|
|
3
|
+
export declare function isAwsCliAvailable(): boolean;
|
|
4
|
+
export declare function awsConfigDir(): string;
|
|
5
|
+
export declare function listAwsProfiles(): string[];
|
|
6
|
+
export declare function hasEnvCredentials(): boolean;
|
|
7
|
+
export declare function envProfile(): string | undefined;
|
|
8
|
+
export declare function envRegion(): string | undefined;
|
|
9
|
+
export declare function configuredRegion(profile?: string): string | undefined;
|
|
10
|
+
export declare function getCallerIdentity(profile: string | undefined, region: string): IdentityResult;
|
|
11
|
+
export interface CredentialWriteResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function setProfileCredentials(profile: string, accessKeyId: string, secretAccessKey: string, region?: string): CredentialWriteResult;
|
|
16
|
+
export declare function credentialsFilePath(): string;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AWS_INSTALL_URL = void 0;
|
|
7
|
+
exports.isAwsCliAvailable = isAwsCliAvailable;
|
|
8
|
+
exports.awsConfigDir = awsConfigDir;
|
|
9
|
+
exports.listAwsProfiles = listAwsProfiles;
|
|
10
|
+
exports.hasEnvCredentials = hasEnvCredentials;
|
|
11
|
+
exports.envProfile = envProfile;
|
|
12
|
+
exports.envRegion = envRegion;
|
|
13
|
+
exports.configuredRegion = configuredRegion;
|
|
14
|
+
exports.getCallerIdentity = getCallerIdentity;
|
|
15
|
+
exports.setProfileCredentials = setProfileCredentials;
|
|
16
|
+
exports.credentialsFilePath = credentialsFilePath;
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const USE_SHELL = process.platform === "win32";
|
|
22
|
+
exports.AWS_INSTALL_URL = "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html";
|
|
23
|
+
function isAwsCliAvailable() {
|
|
24
|
+
const result = (0, node_child_process_1.spawnSync)("aws", ["--version"], {
|
|
25
|
+
stdio: "ignore",
|
|
26
|
+
shell: USE_SHELL,
|
|
27
|
+
});
|
|
28
|
+
return !result.error && result.status === 0;
|
|
29
|
+
}
|
|
30
|
+
function readIfExists(file) {
|
|
31
|
+
try {
|
|
32
|
+
return node_fs_1.default.readFileSync(file, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function awsConfigDir() {
|
|
39
|
+
const configFile = process.env.AWS_CONFIG_FILE;
|
|
40
|
+
return configFile ? node_path_1.default.dirname(configFile) : node_path_1.default.join(node_os_1.default.homedir(), ".aws");
|
|
41
|
+
}
|
|
42
|
+
// Section headers are "[default]" and "[profile foo]" in ~/.aws/config, but plain
|
|
43
|
+
// "[foo]" in ~/.aws/credentials, so the "profile " prefix is optional here.
|
|
44
|
+
function parseProfileNames(contents) {
|
|
45
|
+
const names = [];
|
|
46
|
+
for (const line of contents.split("\n")) {
|
|
47
|
+
const match = /^\s*\[\s*(?:profile\s+)?([^\]\s]+)\s*\]/.exec(line);
|
|
48
|
+
if (match) {
|
|
49
|
+
names.push(match[1]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
function listAwsProfiles() {
|
|
55
|
+
const dir = awsConfigDir();
|
|
56
|
+
const names = [
|
|
57
|
+
...parseProfileNames(readIfExists(node_path_1.default.join(dir, "config"))),
|
|
58
|
+
...parseProfileNames(readIfExists(node_path_1.default.join(dir, "credentials"))),
|
|
59
|
+
];
|
|
60
|
+
return [...new Set(names)].sort();
|
|
61
|
+
}
|
|
62
|
+
function hasEnvCredentials() {
|
|
63
|
+
return Boolean(process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SESSION_TOKEN);
|
|
64
|
+
}
|
|
65
|
+
function envProfile() {
|
|
66
|
+
return process.env.AWS_PROFILE?.trim() || undefined;
|
|
67
|
+
}
|
|
68
|
+
function envRegion() {
|
|
69
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
70
|
+
return region?.trim() || undefined;
|
|
71
|
+
}
|
|
72
|
+
// Asking the AWS CLI lets it apply its own resolution rules (config file, SSO
|
|
73
|
+
// session, env) instead of this command re-deriving them and getting them wrong.
|
|
74
|
+
function configuredRegion(profile) {
|
|
75
|
+
const args = ["configure", "get", "region"];
|
|
76
|
+
if (profile) {
|
|
77
|
+
args.push("--profile", profile);
|
|
78
|
+
}
|
|
79
|
+
const result = (0, node_child_process_1.spawnSync)("aws", args, { encoding: "utf8", shell: USE_SHELL });
|
|
80
|
+
if (result.error || result.status !== 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return result.stdout?.trim() || undefined;
|
|
84
|
+
}
|
|
85
|
+
function getCallerIdentity(profile, region) {
|
|
86
|
+
const args = ["sts", "get-caller-identity", "--output", "json", "--region", region];
|
|
87
|
+
if (profile) {
|
|
88
|
+
args.push("--profile", profile);
|
|
89
|
+
}
|
|
90
|
+
const result = (0, node_child_process_1.spawnSync)("aws", args, { encoding: "utf8", shell: USE_SHELL });
|
|
91
|
+
if (result.error) {
|
|
92
|
+
return { error: result.error.message };
|
|
93
|
+
}
|
|
94
|
+
if (result.status !== 0) {
|
|
95
|
+
return { error: result.stderr?.trim() || `aws exited with code ${result.status ?? 1}` };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(result.stdout);
|
|
99
|
+
if (!parsed.Account || !parsed.Arn) {
|
|
100
|
+
return { error: "sts get-caller-identity returned no account or ARN." };
|
|
101
|
+
}
|
|
102
|
+
return { identity: { account: parsed.Account, arn: parsed.Arn } };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { error: "Could not parse the response from sts get-caller-identity." };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Credentials are handed to the AWS CLI so they land in ~/.aws/credentials in the
|
|
109
|
+
// format AWS expects — never in the project directory, and never in slskit.json.
|
|
110
|
+
// Deliberately runs without a shell so the secret is not exposed to shell parsing.
|
|
111
|
+
function setProfileCredentials(profile, accessKeyId, secretAccessKey, region) {
|
|
112
|
+
const entries = [
|
|
113
|
+
["aws_access_key_id", accessKeyId],
|
|
114
|
+
["aws_secret_access_key", secretAccessKey],
|
|
115
|
+
];
|
|
116
|
+
if (region) {
|
|
117
|
+
entries.push(["region", region]);
|
|
118
|
+
}
|
|
119
|
+
for (const [key, value] of entries) {
|
|
120
|
+
const result = (0, node_child_process_1.spawnSync)("aws", ["configure", "set", key, value, "--profile", profile], { encoding: "utf8" });
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return { ok: false, error: result.error.message };
|
|
123
|
+
}
|
|
124
|
+
if (result.status !== 0) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: result.stderr?.trim() || `aws configure set ${key} exited with ${result.status}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
function credentialsFilePath() {
|
|
134
|
+
return process.env.AWS_SHARED_CREDENTIALS_FILE ?? node_path_1.default.join(awsConfigDir(), "credentials");
|
|
135
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerConfigureCommand = registerConfigureCommand;
|
|
4
|
+
const run_js_1 = require("../../core/run.js");
|
|
5
|
+
const types_js_1 = require("./types.js");
|
|
6
|
+
function registerConfigureCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("configure")
|
|
9
|
+
.description("Set up AWS credentials and the deploy target for an environment")
|
|
10
|
+
.option("-e, --env <name>", `Environment to configure (default: "${types_js_1.DEFAULT_ENVIRONMENT}")`)
|
|
11
|
+
.option("--profile <name>", "AWS named profile to resolve credentials from")
|
|
12
|
+
.option("--region <region>", "AWS region to deploy into (example: us-east-1)")
|
|
13
|
+
.option("--stack-name <name>", "CloudFormation stack name for this environment")
|
|
14
|
+
.option("--skip-verify", "Save without checking that the credentials work", false)
|
|
15
|
+
.option("--set-credentials", "Enter an AWS access key and store it in ~/.aws/credentials", false)
|
|
16
|
+
.action((options) => (0, run_js_1.runCommand)(async () => {
|
|
17
|
+
const { configureAction } = await import("./action.js");
|
|
18
|
+
await configureAction(options);
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { parseEnvironmentName } from "../../core/environments.js";
|
|
2
|
+
import type { ConfigurableManifest, ConfigureAnswers, ConfigureOptions } from "./types.js";
|
|
3
|
+
export declare const ENVIRONMENT_PROFILE = "";
|
|
4
|
+
export declare function parseRegion(value: string | undefined): string | undefined;
|
|
5
|
+
export declare function parseStackName(value: string | undefined): string | undefined;
|
|
6
|
+
export declare function defaultStackName(projectName: string, environment: string): string;
|
|
7
|
+
export declare function collectConfigureAnswers(manifest: ConfigurableManifest, options: ConfigureOptions): Promise<ConfigureAnswers>;
|
|
8
|
+
export { parseEnvironmentName };
|