@pagopa/dx-cli 0.22.4 → 0.23.1
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/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/spec.d.ts +16 -0
- package/dist/adapters/commander/spec.js +54 -0
- package/dist/adapters/plop/generators/environment/actions.js +3 -3
- 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/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 +5 -5
- package/templates/monorepo/nx.json +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.d.ts +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-repository.test.js +0 -95
- package/dist/adapters/azure/__tests__/cloud-account-service.test.d.ts +0 -1
- package/dist/adapters/azure/__tests__/cloud-account-service.test.js +0 -378
- package/dist/adapters/codemods/__tests__/registry.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/registry.test.js +0 -56
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.js +0 -77
- package/dist/adapters/codemods/__tests__/use-pnpm.test.d.ts +0 -1
- package/dist/adapters/codemods/__tests__/use-pnpm.test.js +0 -148
- package/dist/adapters/commander/__tests__/env.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/env.test.js +0 -45
- package/dist/adapters/commander/__tests__/error-reporting.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/error-reporting.test.js +0 -63
- package/dist/adapters/commander/__tests__/exit-with-error.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/exit-with-error.test.js +0 -92
- package/dist/adapters/commander/__tests__/global-options.test.d.ts +0 -1
- package/dist/adapters/commander/__tests__/global-options.test.js +0 -33
- package/dist/adapters/commander/commands/__tests__/add.test.d.ts +0 -4
- package/dist/adapters/commander/commands/__tests__/add.test.js +0 -167
- package/dist/adapters/commander/commands/__tests__/init.test.d.ts +0 -4
- package/dist/adapters/commander/commands/__tests__/init.test.js +0 -48
- package/dist/adapters/commander/commands/__tests__/preconditions.test.d.ts +0 -1
- package/dist/adapters/commander/commands/__tests__/preconditions.test.js +0 -32
- package/dist/adapters/commander/presenters/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/index.test.js +0 -23
- package/dist/adapters/commander/presenters/__tests__/json.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/json.test.js +0 -108
- package/dist/adapters/commander/presenters/__tests__/text.test.d.ts +0 -1
- package/dist/adapters/commander/presenters/__tests__/text.test.js +0 -60
- package/dist/adapters/github/__tests__/github-repo.spec.d.ts +0 -1
- package/dist/adapters/github/__tests__/github-repo.spec.js +0 -67
- package/dist/adapters/node/__tests__/data.d.ts +0 -18
- package/dist/adapters/node/__tests__/data.js +0 -22
- package/dist/adapters/node/__tests__/package-json.test.d.ts +0 -1
- package/dist/adapters/node/__tests__/package-json.test.js +0 -86
- package/dist/adapters/node/__tests__/repository.test.d.ts +0 -1
- package/dist/adapters/node/__tests__/repository.test.js +0 -77
- package/dist/adapters/node/fs/__tests__/file-reader.test.d.ts +0 -1
- package/dist/adapters/node/fs/__tests__/file-reader.test.js +0 -80
- package/dist/adapters/node/json/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/node/json/__tests__/index.test.js +0 -14
- package/dist/adapters/octokit/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/octokit/__tests__/index.test.js +0 -414
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.d.ts +0 -4
- package/dist/adapters/pagopa-technology/__tests__/authorization.test.js +0 -548
- package/dist/adapters/plop/__tests__/run-actions.test.d.ts +0 -1
- package/dist/adapters/plop/__tests__/run-actions.test.js +0 -68
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.d.ts +0 -1
- package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +0 -171
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.d.ts +0 -1
- package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +0 -134
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.d.ts +0 -2
- package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +0 -92
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.d.ts +0 -1
- package/dist/adapters/plop/generators/environment/__tests__/prompts.test.js +0 -182
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.js +0 -113
- package/dist/adapters/plop/helpers/__tests__/terraform-state-key.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/terraform-state-key.test.js +0 -35
- package/dist/adapters/plop/helpers/__tests__/validate-prompt.test.d.ts +0 -1
- package/dist/adapters/plop/helpers/__tests__/validate-prompt.test.js +0 -60
- package/dist/adapters/yaml/__tests__/index.test.d.ts +0 -1
- package/dist/adapters/yaml/__tests__/index.test.js +0 -53
- package/dist/domain/__tests__/data.d.ts +0 -19
- package/dist/domain/__tests__/data.js +0 -28
- package/dist/domain/__tests__/environment.test.d.ts +0 -1
- package/dist/domain/__tests__/environment.test.js +0 -332
- package/dist/domain/__tests__/info.test.d.ts +0 -1
- package/dist/domain/__tests__/info.test.js +0 -77
- package/dist/domain/__tests__/package-json.test.d.ts +0 -1
- package/dist/domain/__tests__/package-json.test.js +0 -39
- package/dist/domain/__tests__/repository.test.d.ts +0 -1
- package/dist/domain/__tests__/repository.test.js +0 -111
- package/dist/domain/__tests__/workspace.test.d.ts +0 -1
- package/dist/domain/__tests__/workspace.test.js +0 -57
- package/dist/use-cases/__tests__/apply-codemod.test.d.ts +0 -1
- package/dist/use-cases/__tests__/apply-codemod.test.js +0 -71
- package/dist/use-cases/__tests__/list-codemods.test.d.ts +0 -1
- package/dist/use-cases/__tests__/list-codemods.test.js +0 -37
- package/dist/use-cases/__tests__/request-authorization.test.d.ts +0 -4
- package/dist/use-cases/__tests__/request-authorization.test.js +0 -43
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">
|
|
@@ -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>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { azure, loadConfig } from "@pagopa/dx-savemoney";
|
|
2
|
-
import { Command } from "commander";
|
|
2
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
3
|
+
import { oraPromise } from "ora";
|
|
4
|
+
import { z } from "zod";
|
|
3
5
|
import { exitWithError } from "../index.js";
|
|
4
6
|
export const makeSavemoneyCommand = () => new Command("savemoney")
|
|
5
7
|
.description("Analyze Azure subscriptions and report unused or inefficient resources")
|
|
@@ -7,7 +9,8 @@ export const makeSavemoneyCommand = () => new Command("savemoney")
|
|
|
7
9
|
.option("-f, --format <format>", "Report format: json, table, detailed-json, or lint (default: table)", "table")
|
|
8
10
|
.option("-l, --location <string>", "Preferred Azure location for resources (overrides config file)", "italynorth")
|
|
9
11
|
.option("-d, --days <number>", "Number of days for metrics analysis (overrides config file)", "30")
|
|
10
|
-
.option("-t, --tags <tags...>", "Filter
|
|
12
|
+
.option("-t, --tags <tags...>", "Filter findings by resource tags (key=value key2=value2). Advisor subscription-level findings remain global.")
|
|
13
|
+
.option("-s, --source <source>", "Restrict findings to a single source: 'advisor', 'custom', or 'all' (default: all)", parseSourceOption, "all")
|
|
11
14
|
.action(async function (options) {
|
|
12
15
|
const { verbose } = this.optsWithGlobals();
|
|
13
16
|
try {
|
|
@@ -19,11 +22,21 @@ export const makeSavemoneyCommand = () => new Command("savemoney")
|
|
|
19
22
|
...config,
|
|
20
23
|
filterTags,
|
|
21
24
|
preferredLocation: options.location || config.preferredLocation,
|
|
25
|
+
sources: options.source === "all"
|
|
26
|
+
? (config.sources ?? ["advisor", "custom"])
|
|
27
|
+
: [options.source],
|
|
22
28
|
timespanDays: Number.parseInt(options.days, 10) || config.timespanDays,
|
|
23
29
|
verbose: verbose ?? false,
|
|
24
30
|
};
|
|
25
|
-
// Run analysis
|
|
26
|
-
|
|
31
|
+
// Run analysis showing a progress spinner on stderr so the CLI doesn't
|
|
32
|
+
// look frozen during the (potentially several-minute) Azure round-trips.
|
|
33
|
+
const reports = await oraPromise(azure.analyzeAzureResources(finalConfig), {
|
|
34
|
+
failText: "Analysis failed",
|
|
35
|
+
stream: process.stderr,
|
|
36
|
+
successText: "Analysis complete",
|
|
37
|
+
text: "Analyzing Azure resources",
|
|
38
|
+
});
|
|
39
|
+
await azure.generateReport(reports, options.format);
|
|
27
40
|
}
|
|
28
41
|
catch (error) {
|
|
29
42
|
exitWithError(this)(error instanceof Error
|
|
@@ -31,12 +44,24 @@ export const makeSavemoneyCommand = () => new Command("savemoney")
|
|
|
31
44
|
: new Error(`Analysis failed: ${String(error)}`));
|
|
32
45
|
}
|
|
33
46
|
});
|
|
47
|
+
const SourceSchema = z.enum(["advisor", "all", "custom"]);
|
|
48
|
+
/**
|
|
49
|
+
* Parses the `--source` option via a zod enum, accepting `all`, `advisor`,
|
|
50
|
+
* or `custom` and rejecting any other value with a Commander-friendly error.
|
|
51
|
+
*/
|
|
52
|
+
export function parseSourceOption(value) {
|
|
53
|
+
const result = SourceSchema.safeParse(value);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
throw new InvalidArgumentError(`Allowed values are: ${SourceSchema.options.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
return result.data;
|
|
58
|
+
}
|
|
34
59
|
/**
|
|
35
60
|
* Parses an array of "key=value" strings (from commander variadic option) into a Map<string, string>.
|
|
36
61
|
* Returns an empty Map when the option is not provided or empty.
|
|
37
62
|
* Supports values that contain "=" (only the first "=" is treated as separator).
|
|
38
63
|
*/
|
|
39
|
-
function parseTagsOption(tagsOption) {
|
|
64
|
+
export function parseTagsOption(tagsOption) {
|
|
40
65
|
const result = new Map();
|
|
41
66
|
if (!tagsOption || tagsOption.length === 0) {
|
|
42
67
|
return result;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Command
|
|
3
|
+
*
|
|
4
|
+
* Implements the `dx spec` subcommand. Calls the injected `getSpec` factory
|
|
5
|
+
* and writes the result as pretty-printed JSON to stdout. The command has no
|
|
6
|
+
* dependencies and requires no authentication.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import type { CliSpec } from "../../../domain/spec.js";
|
|
10
|
+
export declare const makeSpecCommand: (getSpec: () => CliSpec) => Command;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Command
|
|
3
|
+
*
|
|
4
|
+
* Implements the `dx spec` subcommand. Calls the injected `getSpec` factory
|
|
5
|
+
* and writes the result as pretty-printed JSON to stdout. The command has no
|
|
6
|
+
* dependencies and requires no authentication.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export const makeSpecCommand = (getSpec) => new Command("spec")
|
|
10
|
+
.description("Print the full CLI spec as JSON (commands, subcommands, flags, arguments). Useful for agents that need to understand the CLI without running --help on every command.")
|
|
11
|
+
.action(() => {
|
|
12
|
+
process.stdout.write(JSON.stringify(getSpec(), null, 2) + "\n");
|
|
13
|
+
});
|
|
@@ -5,7 +5,9 @@ import { makeDoctorCommand } from "./commands/doctor.js";
|
|
|
5
5
|
import { makeInfoCommand } from "./commands/info.js";
|
|
6
6
|
import { makeInitCommand } from "./commands/init.js";
|
|
7
7
|
import { makeSavemoneyCommand } from "./commands/savemoney.js";
|
|
8
|
+
import { makeSpecCommand } from "./commands/spec.js";
|
|
8
9
|
import { formatErrorDetailed, toErrorMessage } from "./error-reporting.js";
|
|
10
|
+
import { extractCliSpec } from "./spec.js";
|
|
9
11
|
/**
|
|
10
12
|
* Returns true when the global `--verbose` flag is active on the closest
|
|
11
13
|
* ancestor command that defines it (the root `dx` program in our CLI).
|
|
@@ -23,10 +25,12 @@ export const makeCli = (deps, config, cliDeps, version) => {
|
|
|
23
25
|
.default("text"));
|
|
24
26
|
program.addCommand(makeDoctorCommand(deps, config));
|
|
25
27
|
program.addCommand(makeCodemodCommand(cliDeps));
|
|
26
|
-
program.addCommand(makeInitCommand(deps));
|
|
28
|
+
program.addCommand(makeInitCommand(deps.requireGitHubAuth));
|
|
27
29
|
program.addCommand(makeSavemoneyCommand());
|
|
28
30
|
program.addCommand(makeInfoCommand(deps));
|
|
29
|
-
program.addCommand(makeAddCommand(deps));
|
|
31
|
+
program.addCommand(makeAddCommand(deps.requireGitHubAuth));
|
|
32
|
+
// spec is registered last so the closure captures the complete command tree.
|
|
33
|
+
program.addCommand(makeSpecCommand(() => extractCliSpec(program, version)));
|
|
30
34
|
return program;
|
|
31
35
|
};
|
|
32
36
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander Spec Adapter
|
|
3
|
+
*
|
|
4
|
+
* Maps a Commander `Command` tree to the technology-agnostic `CliSpec` domain
|
|
5
|
+
* type defined in `domain/spec.ts`. This is the only place in the codebase
|
|
6
|
+
* that depends on Commander's introspection API.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import type { CliSpec } from "../../domain/spec.js";
|
|
10
|
+
/**
|
|
11
|
+
* Extracts the full CLI spec from a fully-constructed Commander `Command` tree.
|
|
12
|
+
*
|
|
13
|
+
* Should be called after all subcommands have been registered on `rootCommand`
|
|
14
|
+
* so the spec reflects the complete command surface.
|
|
15
|
+
*/
|
|
16
|
+
export declare const extractCliSpec: (rootCommand: Command, version: string) => CliSpec;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander Spec Adapter
|
|
3
|
+
*
|
|
4
|
+
* Maps a Commander `Command` tree to the technology-agnostic `CliSpec` domain
|
|
5
|
+
* type defined in `domain/spec.ts`. This is the only place in the codebase
|
|
6
|
+
* that depends on Commander's introspection API.
|
|
7
|
+
*/
|
|
8
|
+
const INTERNAL_FLAGS = new Set(["--help", "--version"]);
|
|
9
|
+
const INTERNAL_ROOT_COMMANDS = new Set(["spec"]);
|
|
10
|
+
const mapOption = (opt) => ({
|
|
11
|
+
choices: opt.argChoices ?? [],
|
|
12
|
+
defaultValue: opt.defaultValue,
|
|
13
|
+
description: opt.description,
|
|
14
|
+
flags: opt.flags,
|
|
15
|
+
long: opt.long ?? undefined,
|
|
16
|
+
optional: opt.optional,
|
|
17
|
+
required: opt.required,
|
|
18
|
+
short: opt.short ?? undefined,
|
|
19
|
+
});
|
|
20
|
+
const mapArgument = (arg) => ({
|
|
21
|
+
choices: arg.argChoices ?? [],
|
|
22
|
+
defaultValue: arg.defaultValue,
|
|
23
|
+
description: arg.description,
|
|
24
|
+
name: arg.name(),
|
|
25
|
+
required: arg.required,
|
|
26
|
+
variadic: arg.variadic,
|
|
27
|
+
});
|
|
28
|
+
const mapCommand = (cmd) => ({
|
|
29
|
+
arguments: cmd.registeredArguments.map(mapArgument),
|
|
30
|
+
commands: cmd.commands.map(mapCommand),
|
|
31
|
+
description: cmd.description(),
|
|
32
|
+
name: cmd.name(),
|
|
33
|
+
options: cmd.options
|
|
34
|
+
.filter((opt) => !INTERNAL_FLAGS.has(opt.long ?? ""))
|
|
35
|
+
.map(mapOption),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Extracts the full CLI spec from a fully-constructed Commander `Command` tree.
|
|
39
|
+
*
|
|
40
|
+
* Should be called after all subcommands have been registered on `rootCommand`
|
|
41
|
+
* so the spec reflects the complete command surface.
|
|
42
|
+
*/
|
|
43
|
+
export const extractCliSpec = (rootCommand, version) => ({
|
|
44
|
+
commands: rootCommand.commands
|
|
45
|
+
.filter((cmd) => !INTERNAL_ROOT_COMMANDS.has(cmd.name()))
|
|
46
|
+
.map(mapCommand),
|
|
47
|
+
description: rootCommand.description(),
|
|
48
|
+
globalOptions: rootCommand.options
|
|
49
|
+
.filter((opt) => !INTERNAL_FLAGS.has(opt.long ?? ""))
|
|
50
|
+
.map(mapOption),
|
|
51
|
+
name: rootCommand.name(),
|
|
52
|
+
specVersion: "1",
|
|
53
|
+
version,
|
|
54
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { formatTerraformCode } from "../../../terraform/fmt.js";
|
|
4
4
|
import { terraformStateKey } from "../../helpers/terraform-state-key.js";
|
|
5
5
|
import { payloadSchema } from "./prompts.js";
|
|
@@ -11,7 +11,7 @@ const addModule = (context, templatesPath, init = false) => {
|
|
|
11
11
|
return (name) => [
|
|
12
12
|
{
|
|
13
13
|
base: templatesPath,
|
|
14
|
-
data: { cloudAccountsByCsp, includesProdIO, init },
|
|
14
|
+
data: { cloudAccountsByCsp, includesProdIO, init: init || undefined },
|
|
15
15
|
destination: path.join(cwd, "infra"),
|
|
16
16
|
force: true,
|
|
17
17
|
templateFiles: path.join(templatesPath, name),
|
|
@@ -23,7 +23,7 @@ const addModule = (context, templatesPath, init = false) => {
|
|
|
23
23
|
base: path.join(templatesPath, "shared"),
|
|
24
24
|
data: {
|
|
25
25
|
cloudAccountsByCsp,
|
|
26
|
-
init,
|
|
26
|
+
init: init || undefined,
|
|
27
27
|
terraformBackendKey: terraformStateKey(context, name),
|
|
28
28
|
},
|
|
29
29
|
destination: path.join(cwd, "infra", name, "{{env.name}}"),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AzureCliCredential } from "@azure/identity";
|
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
|
3
3
|
import nodePlop from "node-plop";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { Octokit } from "octokit";
|
|
6
5
|
import { oraPromise } from "ora";
|
|
7
6
|
import { RepositoryNotFoundError } from "../../domain/github.js";
|
|
@@ -9,10 +8,10 @@ import { AzureSubscriptionRepository } from "../azure/cloud-account-repository.j
|
|
|
9
8
|
import { AzureCloudAccountService } from "../azure/cloud-account-service.js";
|
|
10
9
|
import createDeploymentEnvironmentGenerator, { payloadSchema as environmentPayloadSchema, PLOP_ENVIRONMENT_GENERATOR_NAME, } from "../plop/generators/environment/index.js";
|
|
11
10
|
import createMonorepoGenerator, { payloadSchema as monorepoPayloadSchema, PLOP_MONOREPO_GENERATOR_NAME, } from "../plop/generators/monorepo/index.js";
|
|
11
|
+
import { resolveTemplatesPath } from "./templates-path.js";
|
|
12
12
|
export const setMonorepoGenerator = (plop) => {
|
|
13
13
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
14
|
-
|
|
15
|
-
createMonorepoGenerator(plop, templatesPath, octokit);
|
|
14
|
+
createMonorepoGenerator(plop, resolveTemplatesPath("monorepo"), octokit);
|
|
16
15
|
};
|
|
17
16
|
const validatePayload = async (payload, github) => {
|
|
18
17
|
try {
|
|
@@ -94,6 +93,5 @@ export const setDeploymentEnvironmentGenerator = (plop, gitHubService, github) =
|
|
|
94
93
|
const credential = new AzureCliCredential();
|
|
95
94
|
const cloudAccountRepository = new AzureSubscriptionRepository(credential);
|
|
96
95
|
const cloudAccountService = new AzureCloudAccountService(credential);
|
|
97
|
-
|
|
98
|
-
createDeploymentEnvironmentGenerator(plop, templatesPath, cloudAccountRepository, cloudAccountService, gitHubService, github);
|
|
96
|
+
createDeploymentEnvironmentGenerator(plop, resolveTemplatesPath("environment"), cloudAccountRepository, cloudAccountService, gitHubService, github);
|
|
99
97
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves CLI plop templates from a single place so runtime wiring and tests
|
|
4
|
+
* stay aligned with the package layout.
|
|
5
|
+
*/
|
|
6
|
+
export const resolveTemplatesPath = (generatorName) => path.join(import.meta.dirname, "../../../templates", generatorName);
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import { ResultAsync } from "neverthrow";
|
|
1
2
|
import { AuthorizationService } from "./authorization.js";
|
|
2
3
|
import { GitHubService } from "./github.js";
|
|
3
4
|
import { PackageJsonReader } from "./package-json.js";
|
|
4
5
|
import { RepositoryReader } from "./repository.js";
|
|
5
6
|
import { ValidationReporter } from "./validation.js";
|
|
6
7
|
export type Dependencies = {
|
|
7
|
-
authorizationService: AuthorizationService;
|
|
8
|
-
gitHubService: GitHubService;
|
|
9
8
|
packageJsonReader: PackageJsonReader;
|
|
10
9
|
repositoryReader: RepositoryReader;
|
|
10
|
+
requireGitHubAuth: GitHubAuthFactory;
|
|
11
11
|
validationReporter: ValidationReporter;
|
|
12
12
|
};
|
|
13
|
+
/** Services that require a GitHub PAT to be instantiated. */
|
|
14
|
+
export type GitHubAuthDeps = {
|
|
15
|
+
authorizationService: AuthorizationService;
|
|
16
|
+
gitHubService: GitHubService;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Lazily resolves GitHub-authenticated services.
|
|
20
|
+
* Commands that need GitHub access call this factory inside their action
|
|
21
|
+
* handler so that auth is only required when those commands actually run.
|
|
22
|
+
* Returns a `ResultAsync` so auth failures (e.g. missing GH_TOKEN) can be
|
|
23
|
+
* routed through the same neverthrow error pipeline used by the commands.
|
|
24
|
+
*/
|
|
25
|
+
export type GitHubAuthFactory = () => ResultAsync<GitHubAuthDeps, Error>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Domain
|
|
3
|
+
*
|
|
4
|
+
* Pure types that describe the shape of the CLI spec output.
|
|
5
|
+
* Technology-agnostic: no dependency on Commander or any CLI framework.
|
|
6
|
+
* Consumed by the Commander adapter and by agent tooling that parses `dx spec`.
|
|
7
|
+
*/
|
|
8
|
+
export type ArgumentSpec = {
|
|
9
|
+
/** Allowed values when the argument is constrained to a set */
|
|
10
|
+
choices: string[];
|
|
11
|
+
defaultValue: unknown;
|
|
12
|
+
description: string;
|
|
13
|
+
name: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
variadic: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type CliSpec = {
|
|
18
|
+
/** Top-level subcommands with their own options, arguments, and nested commands. */
|
|
19
|
+
commands: CommandSpec[];
|
|
20
|
+
description: string;
|
|
21
|
+
/** Options defined directly on the root `dx` program (global flags). */
|
|
22
|
+
globalOptions: OptionSpec[];
|
|
23
|
+
name: string;
|
|
24
|
+
/** Stable version of the spec JSON shape. Increment on breaking changes. */
|
|
25
|
+
specVersion: "1";
|
|
26
|
+
version: string;
|
|
27
|
+
};
|
|
28
|
+
export type CommandSpec = {
|
|
29
|
+
arguments: ArgumentSpec[];
|
|
30
|
+
commands: CommandSpec[];
|
|
31
|
+
description: string;
|
|
32
|
+
name: string;
|
|
33
|
+
options: OptionSpec[];
|
|
34
|
+
};
|
|
35
|
+
export type OptionSpec = {
|
|
36
|
+
/** Allowed values when the option is an enum */
|
|
37
|
+
choices: string[];
|
|
38
|
+
defaultValue: unknown;
|
|
39
|
+
description: string;
|
|
40
|
+
/** Raw flags string as shown in help, e.g. "-v, --verbose" */
|
|
41
|
+
flags: string;
|
|
42
|
+
/** Long flag, e.g. "--verbose" */
|
|
43
|
+
long: string | undefined;
|
|
44
|
+
/** True when the option value is optional (e.g. `--output [mode]`) */
|
|
45
|
+
optional: boolean;
|
|
46
|
+
/** True when the option value is mandatory (e.g. `--output <mode>`) */
|
|
47
|
+
required: boolean;
|
|
48
|
+
/** Short flag, e.g. "-v" (undefined when not present) */
|
|
49
|
+
short: string | undefined;
|
|
50
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "core-js/actual/set/index.js";
|
|
2
2
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
3
|
-
import
|
|
3
|
+
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
|
4
4
|
import { Octokit } from "octokit";
|
|
5
5
|
import codemodRegistry from "./adapters/codemods/index.js";
|
|
6
6
|
import { makeCli } from "./adapters/commander/index.js";
|
|
@@ -13,7 +13,6 @@ import { getConfig } from "./config.js";
|
|
|
13
13
|
import { getInfo } from "./domain/info.js";
|
|
14
14
|
import { applyCodemodById } from "./use-cases/apply-codemod.js";
|
|
15
15
|
import { listCodemods } from "./use-cases/list-codemods.js";
|
|
16
|
-
import { requestAuthorization } from "./use-cases/request-authorization.js";
|
|
17
16
|
/**
|
|
18
17
|
* Returns `true` when `-v` or `--verbose` is present in argv.
|
|
19
18
|
*
|
|
@@ -50,29 +49,33 @@ const configureLogging = async (verbose) => {
|
|
|
50
49
|
};
|
|
51
50
|
export const runCli = async (version) => {
|
|
52
51
|
await configureLogging(detectVerboseFromArgv(process.argv));
|
|
53
|
-
// Creating the adapters
|
|
54
52
|
const repositoryReader = makeRepositoryReader();
|
|
55
53
|
const packageJsonReader = makePackageJsonReader();
|
|
56
54
|
const validationReporter = makeValidationReporter();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Lazily creates GitHub-authenticated services on first call.
|
|
57
|
+
* Only commands that actually need GitHub (init, add) will trigger this,
|
|
58
|
+
* so credential-free commands (spec, doctor, info, …) never require a PAT.
|
|
59
|
+
*/
|
|
60
|
+
const requireGitHubAuth = () => ResultAsync.fromPromise(getGitHubPAT(), (cause) => new Error("Failed to read GitHub PAT", { cause })).andThen((auth) => {
|
|
61
|
+
if (!auth) {
|
|
62
|
+
return errAsync(new Error("GitHub PAT is required. Please set the GH_TOKEN environment variable or login using GitHub CLI."));
|
|
63
|
+
}
|
|
64
|
+
const octokit = new Octokit({ auth });
|
|
65
|
+
const gitHubService = new OctokitGitHubService(octokit);
|
|
66
|
+
const authorizationService = makeAzureAuthorizationService(gitHubService);
|
|
67
|
+
return okAsync({ authorizationService, gitHubService });
|
|
61
68
|
});
|
|
62
|
-
const gitHubService = new OctokitGitHubService(octokit);
|
|
63
|
-
const authorizationService = makeAzureAuthorizationService(gitHubService);
|
|
64
69
|
const deps = {
|
|
65
|
-
authorizationService,
|
|
66
|
-
gitHubService,
|
|
67
70
|
packageJsonReader,
|
|
68
71
|
repositoryReader,
|
|
72
|
+
requireGitHubAuth,
|
|
69
73
|
validationReporter,
|
|
70
74
|
};
|
|
71
75
|
const config = getConfig();
|
|
72
76
|
const useCases = {
|
|
73
77
|
applyCodemodById: applyCodemodById(codemodRegistry, getInfo(deps)),
|
|
74
78
|
listCodemods: listCodemods(codemodRegistry),
|
|
75
|
-
requestAuthorization: requestAuthorization(authorizationService),
|
|
76
79
|
};
|
|
77
80
|
const program = makeCli(deps, config, useCases, version);
|
|
78
81
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagopa/dx-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A CLI useful to manage DX tools.",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"semver": "^7.7.4",
|
|
49
49
|
"yaml": "^2.8.4",
|
|
50
50
|
"zod": "^4.4.2",
|
|
51
|
-
"@pagopa/dx-savemoney": "^0.
|
|
51
|
+
"@pagopa/dx-savemoney": "^0.3.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@tsconfig/node24": "24.0.4",
|
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
"@types/libsodium-wrappers": "^0.8.2",
|
|
57
57
|
"@types/node": "^22.19.17",
|
|
58
58
|
"@types/semver": "^7.7.1",
|
|
59
|
-
"@vitest/coverage-v8": "^
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
60
60
|
"eslint": "^10.3.0",
|
|
61
61
|
"fast-check": "^4.8.0",
|
|
62
62
|
"memfs": "^4.57.2",
|
|
63
63
|
"plop": "^4.0.5",
|
|
64
64
|
"prettier": "3.8.3",
|
|
65
65
|
"typescript": "~5.9.3",
|
|
66
|
-
"vitest": "^
|
|
67
|
-
"vitest-mock-extended": "^
|
|
66
|
+
"vitest": "^4.1.8",
|
|
67
|
+
"vitest-mock-extended": "^4.0.0",
|
|
68
68
|
"@pagopa/eslint-config": "^6.0.4"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|