@pagopa/dx-cli 0.22.3 โ 0.23.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/README.md +18 -0
- package/dist/adapters/commander/__tests__/spec.test.d.ts +8 -0
- package/dist/adapters/commander/__tests__/spec.test.js +264 -0
- package/dist/adapters/commander/commands/__tests__/init-command.test.d.ts +4 -0
- package/dist/adapters/commander/commands/__tests__/init-command.test.js +44 -0
- package/dist/adapters/commander/commands/__tests__/savemoney.test.d.ts +8 -0
- package/dist/adapters/commander/commands/__tests__/savemoney.test.js +60 -0
- package/dist/adapters/commander/commands/__tests__/spec.test.d.ts +7 -0
- package/dist/adapters/commander/commands/__tests__/spec.test.js +85 -0
- package/dist/adapters/commander/commands/add.d.ts +2 -6
- package/dist/adapters/commander/commands/add.js +4 -8
- package/dist/adapters/commander/commands/init.d.ts +2 -6
- package/dist/adapters/commander/commands/init.js +6 -5
- package/dist/adapters/commander/commands/savemoney.d.ts +11 -0
- package/dist/adapters/commander/commands/savemoney.js +30 -5
- package/dist/adapters/commander/commands/spec.d.ts +10 -0
- package/dist/adapters/commander/commands/spec.js +13 -0
- package/dist/adapters/commander/index.js +6 -2
- package/dist/adapters/commander/presenters/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/index.test.js +23 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.js +108 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.js +60 -0
- package/dist/adapters/commander/presenters/index.d.ts +27 -0
- package/dist/adapters/commander/presenters/index.js +16 -0
- package/dist/adapters/commander/presenters/json-command-presenter.d.ts +19 -0
- package/dist/adapters/commander/presenters/json-command-presenter.js +26 -0
- package/dist/adapters/commander/presenters/text-command-presenter.d.ts +6 -0
- package/dist/adapters/commander/presenters/text-command-presenter.js +21 -0
- package/dist/adapters/commander/spec.d.ts +16 -0
- package/dist/adapters/commander/spec.js +54 -0
- package/dist/adapters/plop/generators/__tests__/temp-dir.d.ts +2 -0
- package/dist/adapters/plop/generators/__tests__/temp-dir.js +13 -0
- package/dist/adapters/plop/generators/environment/__tests__/generation.test.d.ts +1 -0
- package/dist/adapters/plop/generators/environment/__tests__/generation.test.js +213 -0
- package/dist/adapters/plop/generators/environment/actions.js +3 -3
- package/dist/adapters/plop/generators/monorepo/__tests__/generation.test.d.ts +1 -0
- package/dist/adapters/plop/generators/monorepo/__tests__/generation.test.js +79 -0
- package/dist/adapters/plop/index.js +3 -5
- package/dist/adapters/plop/templates-path.d.ts +5 -0
- package/dist/adapters/plop/templates-path.js +6 -0
- package/dist/domain/__tests__/data.d.ts +3 -7
- package/dist/domain/__tests__/data.js +2 -4
- package/dist/domain/dependencies.d.ts +15 -2
- package/dist/domain/spec.d.ts +50 -0
- package/dist/domain/spec.js +8 -0
- package/dist/index.js +15 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -185,6 +185,7 @@ dx savemoney [options]
|
|
|
185
185
|
| `--days` | `-d` | Metric analysis period in days (overrides config file). | `30` |
|
|
186
186
|
| `--location` | `-l` | Preferred Azure location for resources (overrides config file). | `italynorth` |
|
|
187
187
|
| `--tags` | `-t` | Filter resources by tags (`key=value key2=value2`). Only resources matching **all** specified tags are analyzed (variadic: space-separated). | N/A |
|
|
188
|
+
| `--source` | `-s` | Restrict findings to a specific source: `advisor`, `custom`, or `all`. | `all` |
|
|
188
189
|
|
|
189
190
|
> `--verbose` / `-v` is inherited from the root command. See [Global Options](#global-options).
|
|
190
191
|
|
|
@@ -240,6 +241,7 @@ azure:
|
|
|
240
241
|
- **Private Endpoints**: Unused or misconfigured endpoints
|
|
241
242
|
- **Container Apps**: Not running, zero replicas, low resource usage
|
|
242
243
|
- **Static Web Apps**: No traffic or very low usage patterns
|
|
244
|
+
- **Azure Advisor recommendations**: Reserved Instance and Savings Plan opportunities, right-sizing suggestions, and other cost recommendations surfaced directly from Azure Advisor โ with estimated monthly savings where available
|
|
243
245
|
|
|
244
246
|
> [!NOTE]
|
|
245
247
|
> Currently only Azure is supported. Support for additional cloud providers (AWS) is planned for future releases.
|
|
@@ -259,6 +261,22 @@ These options are available on every subcommand:
|
|
|
259
261
|
dx --verbose init project
|
|
260
262
|
```
|
|
261
263
|
|
|
264
|
+
## ๐งช Development
|
|
265
|
+
|
|
266
|
+
Run the CLI test suite from the repository root with:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pnpm nx test @pagopa/dx-cli
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Some integration tests assert generated file contents with Vitest snapshots. When a generator change intentionally updates those files, refresh the snapshots with:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
pnpm nx test @pagopa/dx-cli -- --update
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Review the updated `__snapshots__` files before committing them.
|
|
279
|
+
|
|
262
280
|
---
|
|
263
281
|
|
|
264
282
|
<div align="center">
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Commander spec adapter.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `extractCliSpec` correctly maps Commander's internal command
|
|
5
|
+
* tree to the `CliSpec` domain type, covering options, arguments, and nested
|
|
6
|
+
* subcommands.
|
|
7
|
+
*/
|
|
8
|
+
import { Argument, Command, Option } from "commander";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { extractCliSpec } from "../spec.js";
|
|
11
|
+
const buildRoot = () => new Command("dx")
|
|
12
|
+
.description("The CLI for DX-Platform")
|
|
13
|
+
.version("1.2.3")
|
|
14
|
+
.addOption(new Option("-v, --verbose", "Enable verbose output").default(false))
|
|
15
|
+
.addOption(new Option("--output <mode>", "Output mode")
|
|
16
|
+
.choices(["text", "json"])
|
|
17
|
+
.default("text"));
|
|
18
|
+
const registerRootMetadataAndGlobalOptionTests = () => {
|
|
19
|
+
describe("root metadata and global options", () => {
|
|
20
|
+
it("maps the root command name, description and version", () => {
|
|
21
|
+
const root = buildRoot();
|
|
22
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
23
|
+
expect(spec).toMatchObject({
|
|
24
|
+
description: "The CLI for DX-Platform",
|
|
25
|
+
name: "dx",
|
|
26
|
+
specVersion: "1",
|
|
27
|
+
version: "1.2.3",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("maps global options from the root command", () => {
|
|
31
|
+
const root = buildRoot();
|
|
32
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
33
|
+
expect(spec.globalOptions.find((o) => o.long === "--verbose")).toStrictEqual({
|
|
34
|
+
choices: [],
|
|
35
|
+
defaultValue: false,
|
|
36
|
+
description: "Enable verbose output",
|
|
37
|
+
flags: "-v, --verbose",
|
|
38
|
+
long: "--verbose",
|
|
39
|
+
optional: false,
|
|
40
|
+
required: false,
|
|
41
|
+
short: "-v",
|
|
42
|
+
});
|
|
43
|
+
expect(spec.globalOptions.find((o) => o.long === "--output")).toStrictEqual({
|
|
44
|
+
choices: ["text", "json"],
|
|
45
|
+
defaultValue: "text",
|
|
46
|
+
description: "Output mode",
|
|
47
|
+
flags: "--output <mode>",
|
|
48
|
+
long: "--output",
|
|
49
|
+
optional: false,
|
|
50
|
+
required: true,
|
|
51
|
+
short: undefined,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("excludes help and version options from globalOptions", () => {
|
|
55
|
+
const root = buildRoot();
|
|
56
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
57
|
+
const flags = spec.globalOptions.map((o) => o.long);
|
|
58
|
+
expect(flags).not.toContain("--help");
|
|
59
|
+
expect(flags).not.toContain("--version");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const registerCommandListTests = () => {
|
|
64
|
+
describe("command lists", () => {
|
|
65
|
+
it("maps a flat subcommand with options and no arguments", () => {
|
|
66
|
+
const root = buildRoot();
|
|
67
|
+
root.addCommand(new Command("doctor")
|
|
68
|
+
.description("Run health checks")
|
|
69
|
+
.addOption(new Option("--fix", "Auto-fix issues").default(false)));
|
|
70
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
71
|
+
expect(spec.commands).toStrictEqual([
|
|
72
|
+
{
|
|
73
|
+
arguments: [],
|
|
74
|
+
commands: [],
|
|
75
|
+
description: "Run health checks",
|
|
76
|
+
name: "doctor",
|
|
77
|
+
options: [
|
|
78
|
+
{
|
|
79
|
+
choices: [],
|
|
80
|
+
defaultValue: false,
|
|
81
|
+
description: "Auto-fix issues",
|
|
82
|
+
flags: "--fix",
|
|
83
|
+
long: "--fix",
|
|
84
|
+
optional: false,
|
|
85
|
+
required: false,
|
|
86
|
+
short: undefined,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it("maps a subcommand argument (required)", () => {
|
|
93
|
+
const root = buildRoot();
|
|
94
|
+
root.addCommand(new Command("apply")
|
|
95
|
+
.description("Apply something")
|
|
96
|
+
.addArgument(new Argument("<id>", "The id of the item to apply")));
|
|
97
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
98
|
+
expect(spec.commands).toStrictEqual([
|
|
99
|
+
{
|
|
100
|
+
arguments: [
|
|
101
|
+
{
|
|
102
|
+
choices: [],
|
|
103
|
+
defaultValue: undefined,
|
|
104
|
+
description: "The id of the item to apply",
|
|
105
|
+
name: "id",
|
|
106
|
+
required: true,
|
|
107
|
+
variadic: false,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
commands: [],
|
|
111
|
+
description: "Apply something",
|
|
112
|
+
name: "apply",
|
|
113
|
+
options: [],
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
it("maps a variadic optional argument", () => {
|
|
118
|
+
const root = buildRoot();
|
|
119
|
+
root.addCommand(new Command("run")
|
|
120
|
+
.description("Run scripts")
|
|
121
|
+
.addArgument(new Argument("[scripts...]", "Scripts to run")));
|
|
122
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
123
|
+
expect(spec.commands).toStrictEqual([
|
|
124
|
+
{
|
|
125
|
+
arguments: [
|
|
126
|
+
{
|
|
127
|
+
choices: [],
|
|
128
|
+
defaultValue: undefined,
|
|
129
|
+
description: "Scripts to run",
|
|
130
|
+
name: "scripts",
|
|
131
|
+
required: false,
|
|
132
|
+
variadic: true,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
commands: [],
|
|
136
|
+
description: "Run scripts",
|
|
137
|
+
name: "run",
|
|
138
|
+
options: [],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
it("includes commands registered with { hidden: true }", () => {
|
|
143
|
+
const root = buildRoot();
|
|
144
|
+
const visible = new Command("visible").description("Visible");
|
|
145
|
+
const hidden = new Command("internal").description("Internal");
|
|
146
|
+
root.addCommand(visible);
|
|
147
|
+
root.addCommand(hidden, { hidden: true });
|
|
148
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
149
|
+
expect(spec.commands).toStrictEqual([
|
|
150
|
+
{
|
|
151
|
+
arguments: [],
|
|
152
|
+
commands: [],
|
|
153
|
+
description: "Visible",
|
|
154
|
+
name: "visible",
|
|
155
|
+
options: [],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
arguments: [],
|
|
159
|
+
commands: [],
|
|
160
|
+
description: "Internal",
|
|
161
|
+
name: "internal",
|
|
162
|
+
options: [],
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
it("excludes the spec command from the extracted command list", () => {
|
|
167
|
+
const root = buildRoot();
|
|
168
|
+
root.addCommand(new Command("doctor").description("Run health checks"));
|
|
169
|
+
root.addCommand(new Command("spec").description("Print the CLI spec"));
|
|
170
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
171
|
+
expect(spec.commands).toStrictEqual([
|
|
172
|
+
{
|
|
173
|
+
arguments: [],
|
|
174
|
+
commands: [],
|
|
175
|
+
description: "Run health checks",
|
|
176
|
+
name: "doctor",
|
|
177
|
+
options: [],
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
it("returns empty commands array when root has no subcommands", () => {
|
|
182
|
+
const root = buildRoot();
|
|
183
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
184
|
+
expect(spec.commands).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
it("maps an argument with choices", () => {
|
|
187
|
+
const root = buildRoot();
|
|
188
|
+
root.addCommand(new Command("format")
|
|
189
|
+
.description("Format output")
|
|
190
|
+
.addArgument(new Argument("<style>", "Output style").choices(["json", "table"])));
|
|
191
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
192
|
+
expect(spec.commands).toStrictEqual([
|
|
193
|
+
{
|
|
194
|
+
arguments: [
|
|
195
|
+
{
|
|
196
|
+
choices: ["json", "table"],
|
|
197
|
+
defaultValue: undefined,
|
|
198
|
+
description: "Output style",
|
|
199
|
+
name: "style",
|
|
200
|
+
required: true,
|
|
201
|
+
variadic: false,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
commands: [],
|
|
205
|
+
description: "Format output",
|
|
206
|
+
name: "format",
|
|
207
|
+
options: [],
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const registerNestedCommandTests = () => {
|
|
214
|
+
describe("nested commands", () => {
|
|
215
|
+
it("recursively maps nested subcommands", () => {
|
|
216
|
+
const root = buildRoot();
|
|
217
|
+
const codemod = new Command("codemod").description("Manage codemods");
|
|
218
|
+
codemod.addCommand(new Command("list").description("List available codemods"));
|
|
219
|
+
codemod.addCommand(new Command("apply")
|
|
220
|
+
.description("Apply a codemod")
|
|
221
|
+
.addArgument(new Argument("<id>", "Codemod id")));
|
|
222
|
+
root.addCommand(codemod);
|
|
223
|
+
const spec = extractCliSpec(root, "1.2.3");
|
|
224
|
+
expect(spec.commands).toStrictEqual([
|
|
225
|
+
{
|
|
226
|
+
arguments: [],
|
|
227
|
+
commands: [
|
|
228
|
+
{
|
|
229
|
+
arguments: [],
|
|
230
|
+
commands: [],
|
|
231
|
+
description: "List available codemods",
|
|
232
|
+
name: "list",
|
|
233
|
+
options: [],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
arguments: [
|
|
237
|
+
{
|
|
238
|
+
choices: [],
|
|
239
|
+
defaultValue: undefined,
|
|
240
|
+
description: "Codemod id",
|
|
241
|
+
name: "id",
|
|
242
|
+
required: true,
|
|
243
|
+
variadic: false,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
commands: [],
|
|
247
|
+
description: "Apply a codemod",
|
|
248
|
+
name: "apply",
|
|
249
|
+
options: [],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
description: "Manage codemods",
|
|
253
|
+
name: "codemod",
|
|
254
|
+
options: [],
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
describe("extractCliSpec", () => {
|
|
261
|
+
registerRootMetadataAndGlobalOptionTests();
|
|
262
|
+
registerCommandListTests();
|
|
263
|
+
registerNestedCommandTests();
|
|
264
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the init Commander command workflow.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { okAsync } from "neverthrow";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { mock } from "vitest-mock-extended";
|
|
8
|
+
const { tfMock } = vi.hoisted(() => ({
|
|
9
|
+
tfMock: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("ora", () => ({
|
|
12
|
+
oraPromise: (promise) => promise,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../../../execa/terraform.js", () => ({
|
|
15
|
+
tf$: tfMock,
|
|
16
|
+
}));
|
|
17
|
+
import { makeInitCommand } from "../init.js";
|
|
18
|
+
const makeRoot = (command) => new Command()
|
|
19
|
+
.exitOverride()
|
|
20
|
+
.addCommand(command)
|
|
21
|
+
.configureOutput({
|
|
22
|
+
writeErr: () => {
|
|
23
|
+
/* silence stderr in tests */
|
|
24
|
+
},
|
|
25
|
+
writeOut: () => {
|
|
26
|
+
/* silence stdout in tests */
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
describe("makeInitCommand", () => {
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
tfMock.mockReset();
|
|
32
|
+
});
|
|
33
|
+
it("checks local preconditions before requiring GitHub auth", async () => {
|
|
34
|
+
tfMock.mockRejectedValueOnce(new Error("Terraform not installed"));
|
|
35
|
+
const requireGitHubAuthSpy = vi.fn(() => okAsync({
|
|
36
|
+
authorizationService: mock(),
|
|
37
|
+
gitHubService: mock(),
|
|
38
|
+
}));
|
|
39
|
+
const requireGitHubAuth = requireGitHubAuthSpy;
|
|
40
|
+
const root = makeRoot(makeInitCommand(requireGitHubAuth));
|
|
41
|
+
await expect(root.parseAsync(["init"], { from: "user" })).rejects.toBeTruthy();
|
|
42
|
+
expect(requireGitHubAuthSpy).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pure helper functions in the savemoney command.
|
|
3
|
+
*
|
|
4
|
+
* The full command action (Azure API calls, spinner) is not exercised here
|
|
5
|
+
* because it requires live credentials. These tests cover the stateless
|
|
6
|
+
* parsing helpers that can be validated without any I/O.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pure helper functions in the savemoney command.
|
|
3
|
+
*
|
|
4
|
+
* The full command action (Azure API calls, spinner) is not exercised here
|
|
5
|
+
* because it requires live credentials. These tests cover the stateless
|
|
6
|
+
* parsing helpers that can be validated without any I/O.
|
|
7
|
+
*/
|
|
8
|
+
import { InvalidArgumentError } from "commander";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { makeSavemoneyCommand, parseSourceOption, parseTagsOption, } from "../savemoney.js";
|
|
11
|
+
describe("parseSourceOption", () => {
|
|
12
|
+
it("accepts 'advisor'", () => {
|
|
13
|
+
expect(parseSourceOption("advisor")).toBe("advisor");
|
|
14
|
+
});
|
|
15
|
+
it("accepts 'custom'", () => {
|
|
16
|
+
expect(parseSourceOption("custom")).toBe("custom");
|
|
17
|
+
});
|
|
18
|
+
it("accepts 'all'", () => {
|
|
19
|
+
expect(parseSourceOption("all")).toBe("all");
|
|
20
|
+
});
|
|
21
|
+
it("throws InvalidArgumentError for an unknown value", () => {
|
|
22
|
+
expect(() => parseSourceOption("aws")).toThrow(InvalidArgumentError);
|
|
23
|
+
});
|
|
24
|
+
it("throws InvalidArgumentError for an empty string", () => {
|
|
25
|
+
expect(() => parseSourceOption("")).toThrow(InvalidArgumentError);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("parseTagsOption", () => {
|
|
29
|
+
it("returns an empty Map when tagsOption is undefined", () => {
|
|
30
|
+
expect(parseTagsOption(undefined)).toEqual(new Map());
|
|
31
|
+
});
|
|
32
|
+
it("returns an empty Map when tagsOption is an empty array", () => {
|
|
33
|
+
expect(parseTagsOption([])).toEqual(new Map());
|
|
34
|
+
});
|
|
35
|
+
it("parses a single key=value pair", () => {
|
|
36
|
+
expect(parseTagsOption(["env=dev"])).toEqual(new Map([["env", "dev"]]));
|
|
37
|
+
});
|
|
38
|
+
it("parses multiple key=value pairs", () => {
|
|
39
|
+
expect(parseTagsOption(["env=dev", "team=platform"])).toEqual(new Map([
|
|
40
|
+
["env", "dev"],
|
|
41
|
+
["team", "platform"],
|
|
42
|
+
]));
|
|
43
|
+
});
|
|
44
|
+
it("handles values that contain '='", () => {
|
|
45
|
+
expect(parseTagsOption(["url=https://example.com/path=1"])).toEqual(new Map([["url", "https://example.com/path=1"]]));
|
|
46
|
+
});
|
|
47
|
+
it("ignores entries without a '=' separator", () => {
|
|
48
|
+
expect(parseTagsOption(["noequalssign"])).toEqual(new Map());
|
|
49
|
+
});
|
|
50
|
+
it("trims whitespace from keys and values", () => {
|
|
51
|
+
expect(parseTagsOption([" env = dev "])).toEqual(new Map([["env", "dev"]]));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("makeSavemoneyCommand", () => {
|
|
55
|
+
it("documents that --tags does not filter subscription-level Advisor findings", () => {
|
|
56
|
+
const command = makeSavemoneyCommand();
|
|
57
|
+
const tagsOption = command.options.find((option) => option.flags.includes("--tags"));
|
|
58
|
+
expect(tagsOption?.description).toContain("Advisor subscription-level findings remain global.");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `spec` Commander command.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `makeSpecCommand` calls the `getSpec` callback and writes the
|
|
5
|
+
* resulting CliSpec as pretty-printed JSON to stdout.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
|
|
9
|
+
import { makeSpecCommand } from "../spec.js";
|
|
10
|
+
const makeMinimalSpec = () => ({
|
|
11
|
+
commands: [],
|
|
12
|
+
description: "The CLI for DX-Platform",
|
|
13
|
+
globalOptions: [],
|
|
14
|
+
name: "dx",
|
|
15
|
+
specVersion: "1",
|
|
16
|
+
version: "0.0.0",
|
|
17
|
+
});
|
|
18
|
+
describe("makeSpecCommand", () => {
|
|
19
|
+
let stdoutSpy;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
stdoutSpy = vi
|
|
22
|
+
.spyOn(process.stdout, "write")
|
|
23
|
+
.mockImplementation(() => true);
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
stdoutSpy.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
it("is registered as the 'spec' subcommand", () => {
|
|
29
|
+
const cmd = makeSpecCommand(() => makeMinimalSpec());
|
|
30
|
+
expect(cmd.name()).toBe("spec");
|
|
31
|
+
});
|
|
32
|
+
it("calls getSpec and writes JSON to stdout when invoked", async () => {
|
|
33
|
+
const spec = makeMinimalSpec();
|
|
34
|
+
const getSpec = vi.fn().mockReturnValue(spec);
|
|
35
|
+
const cmd = makeSpecCommand(getSpec);
|
|
36
|
+
// Parse with an isolated parent so Commander doesn't exit
|
|
37
|
+
const root = new Command().exitOverride().addCommand(cmd);
|
|
38
|
+
root.configureOutput({
|
|
39
|
+
writeErr: () => {
|
|
40
|
+
/* silence */
|
|
41
|
+
},
|
|
42
|
+
writeOut: () => {
|
|
43
|
+
/* silence */
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
await root.parseAsync(["spec"], { from: "user" });
|
|
47
|
+
expect(getSpec).toHaveBeenCalledOnce();
|
|
48
|
+
const written = stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
49
|
+
expect(JSON.parse(written)).toEqual(spec);
|
|
50
|
+
});
|
|
51
|
+
it("outputs valid pretty-printed JSON (indented)", async () => {
|
|
52
|
+
const spec = makeMinimalSpec();
|
|
53
|
+
const cmd = makeSpecCommand(() => spec);
|
|
54
|
+
const root = new Command().exitOverride().addCommand(cmd);
|
|
55
|
+
root.configureOutput({
|
|
56
|
+
writeErr: () => {
|
|
57
|
+
/* silence */
|
|
58
|
+
},
|
|
59
|
+
writeOut: () => {
|
|
60
|
+
/* silence */
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
await root.parseAsync(["spec"], { from: "user" });
|
|
64
|
+
const written = stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
65
|
+
// Pretty-printed JSON has newlines
|
|
66
|
+
expect(written).toContain("\n");
|
|
67
|
+
expect(written).toContain(" ");
|
|
68
|
+
});
|
|
69
|
+
it("includes specVersion '1' in the output", async () => {
|
|
70
|
+
const cmd = makeSpecCommand(() => makeMinimalSpec());
|
|
71
|
+
const root = new Command().exitOverride().addCommand(cmd);
|
|
72
|
+
root.configureOutput({
|
|
73
|
+
writeErr: () => {
|
|
74
|
+
/* silence */
|
|
75
|
+
},
|
|
76
|
+
writeOut: () => {
|
|
77
|
+
/* silence */
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
await root.parseAsync(["spec"], { from: "user" });
|
|
81
|
+
const written = stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
82
|
+
const parsed = JSON.parse(written);
|
|
83
|
+
expect(parsed.specVersion).toBe("1");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -11,13 +11,9 @@ import { Command } from "commander";
|
|
|
11
11
|
import { ResultAsync } from "neverthrow";
|
|
12
12
|
import type { Payload as EnvironmentPayload } from "../../plop/generators/environment/index.js";
|
|
13
13
|
import { AuthorizationResult, AuthorizationService } from "../../../domain/authorization.js";
|
|
14
|
-
import {
|
|
14
|
+
import { GitHubAuthFactory } from "../../../domain/dependencies.js";
|
|
15
15
|
/**
|
|
16
16
|
* Authorize a Cloud Account (Azure Subscription, AWS Account, ...), creating a Pull Request for each account that requires authorization.
|
|
17
17
|
*/
|
|
18
18
|
export declare const authorizeCloudAccounts: (authorizationService: AuthorizationService) => (envPayload: EnvironmentPayload) => ResultAsync<AuthorizationResult[], never>;
|
|
19
|
-
export
|
|
20
|
-
authorizationService: AuthorizationService;
|
|
21
|
-
gitHubService: GitHubService;
|
|
22
|
-
};
|
|
23
|
-
export declare const makeAddCommand: (deps: AddCommandDependencies) => Command;
|
|
19
|
+
export declare const makeAddCommand: (requireGitHubAuth: GitHubAuthFactory) => Command;
|
|
@@ -81,17 +81,13 @@ const addEnvironmentAction = (authorizationService, gitHubService) => checkAddEn
|
|
|
81
81
|
.andThen((payload) => authorizeCloudAccounts(authorizationService)(payload).map((authorizationPrs) => ({
|
|
82
82
|
authorizationPrs,
|
|
83
83
|
})));
|
|
84
|
-
export const makeAddCommand = (
|
|
84
|
+
export const makeAddCommand = (requireGitHubAuth) => new Command()
|
|
85
85
|
.name("add")
|
|
86
86
|
.description("Add a new component to your workspace")
|
|
87
87
|
.addCommand(new Command("environment")
|
|
88
88
|
.description("Add a new deployment environment")
|
|
89
89
|
.action(async function () {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
exitWithError(this)
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
displaySummary(result.value);
|
|
96
|
-
}
|
|
90
|
+
await requireGitHubAuth()
|
|
91
|
+
.andThen(({ authorizationService, gitHubService }) => addEnvironmentAction(authorizationService, gitHubService))
|
|
92
|
+
.match(displaySummary, exitWithError(this));
|
|
97
93
|
}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { ResultAsync } from "neverthrow";
|
|
3
|
-
import {
|
|
3
|
+
import { GitHubAuthFactory } from "../../../domain/dependencies.js";
|
|
4
4
|
import { Payload as MonorepoPayload } from "../../plop/generators/monorepo/index.js";
|
|
5
5
|
export declare const checkInitPreconditions: () => ResultAsync<import("execa").Result<{
|
|
6
6
|
environment: {
|
|
@@ -19,8 +19,4 @@ export declare const checkAddEnvironmentPreconditions: () => ResultAsync<import(
|
|
|
19
19
|
shell: true;
|
|
20
20
|
}>, Error>;
|
|
21
21
|
export declare const confirmGitHubRepoCreation: (payload: MonorepoPayload) => ResultAsync<boolean, Error>;
|
|
22
|
-
|
|
23
|
-
gitHubService: GitHubService;
|
|
24
|
-
};
|
|
25
|
-
export declare const makeInitCommand: ({ gitHubService, }: InitCommandDependencies) => Command;
|
|
26
|
-
export {};
|
|
22
|
+
export declare const makeInitCommand: (requireGitHubAuth: GitHubAuthFactory) => Command;
|
|
@@ -142,18 +142,19 @@ export const confirmGitHubRepoCreation = (payload) => ResultAsync.fromPromise(in
|
|
|
142
142
|
type: "confirm",
|
|
143
143
|
})
|
|
144
144
|
.then(({ confirm }) => confirm), (cause) => new Error("Failed to read GitHub publish confirmation", { cause }));
|
|
145
|
-
export const makeInitCommand = (
|
|
145
|
+
export const makeInitCommand = (requireGitHubAuth) => new Command()
|
|
146
146
|
.name("init")
|
|
147
147
|
.description("Initialize a new DX workspace")
|
|
148
148
|
.action(async function () {
|
|
149
149
|
await checkInitPreconditions()
|
|
150
|
-
.andThen(() =>
|
|
151
|
-
.andThen((
|
|
150
|
+
.andThen(() => requireGitHubAuth())
|
|
151
|
+
.andThen((auth) => ResultAsync.fromPromise(getPlopInstance(), (cause) => new Error("Failed to initialize plop", { cause }))
|
|
152
|
+
.andThen((plop) => ResultAsync.fromPromise(runMonorepoGenerator(plop, auth.gitHubService), handleGeneratorError))
|
|
152
153
|
.andTee((payload) => {
|
|
153
154
|
process.chdir(payload.repoName);
|
|
154
155
|
})
|
|
155
156
|
.andThen((payload) => confirmGitHubRepoCreation(payload).andThen((confirmed) => confirmed
|
|
156
|
-
? handleNewGitHubRepository(gitHubService)(payload)
|
|
157
|
-
: okAsync({ gitHubRepoCreationSkipped: true, payload })))
|
|
157
|
+
? handleNewGitHubRepository(auth.gitHubService)(payload)
|
|
158
|
+
: okAsync({ gitHubRepoCreationSkipped: true, payload }))))
|
|
158
159
|
.match(displaySummary, exitWithError(this));
|
|
159
160
|
});
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare const makeSavemoneyCommand: () => Command;
|
|
3
|
+
/**
|
|
4
|
+
* Parses the `--source` option via a zod enum, accepting `all`, `advisor`,
|
|
5
|
+
* or `custom` and rejecting any other value with a Commander-friendly error.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseSourceOption(value: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Parses an array of "key=value" strings (from commander variadic option) into a Map<string, string>.
|
|
10
|
+
* Returns an empty Map when the option is not provided or empty.
|
|
11
|
+
* Supports values that contain "=" (only the first "=" is treated as separator).
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseTagsOption(tagsOption: string[] | undefined): Map<string, string>;
|