@pagopa/dx-cli 0.15.1 → 0.15.3

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.
Files changed (195) hide show
  1. package/bin/index.js +8 -1280
  2. package/dist/adapters/azure/__tests__/cloud-account-repository.test.d.ts +1 -0
  3. package/dist/adapters/azure/__tests__/cloud-account-repository.test.js +95 -0
  4. package/dist/adapters/azure/__tests__/cloud-account-service.test.d.ts +1 -0
  5. package/dist/adapters/azure/__tests__/cloud-account-service.test.js +95 -0
  6. package/dist/adapters/azure/cloud-account-repository.d.ts +12 -0
  7. package/dist/adapters/azure/cloud-account-repository.js +23 -0
  8. package/dist/adapters/azure/cloud-account-service.d.ts +22 -0
  9. package/dist/adapters/azure/cloud-account-service.js +255 -0
  10. package/dist/adapters/azure/locations.d.ts +7 -0
  11. package/dist/adapters/azure/locations.js +21 -0
  12. package/dist/adapters/codemods/__tests__/registry.test.d.ts +1 -0
  13. package/dist/adapters/codemods/__tests__/registry.test.js +59 -0
  14. package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.d.ts +1 -0
  15. package/dist/adapters/codemods/__tests__/use-azure-appsvc.test.js +77 -0
  16. package/dist/adapters/codemods/__tests__/use-pnpm.test.d.ts +1 -0
  17. package/dist/adapters/codemods/__tests__/use-pnpm.test.js +148 -0
  18. package/dist/adapters/codemods/git.d.ts +2 -0
  19. package/dist/adapters/codemods/git.js +18 -0
  20. package/dist/adapters/codemods/index.d.ts +3 -0
  21. package/dist/adapters/codemods/index.js +9 -0
  22. package/dist/adapters/codemods/registry.d.ts +8 -0
  23. package/dist/adapters/codemods/registry.js +16 -0
  24. package/dist/adapters/codemods/update-code-review.d.ts +3 -0
  25. package/dist/adapters/codemods/update-code-review.js +60 -0
  26. package/dist/adapters/codemods/use-azure-appsvc.d.ts +3 -0
  27. package/dist/adapters/codemods/use-azure-appsvc.js +84 -0
  28. package/dist/adapters/codemods/use-pnpm.d.ts +22 -0
  29. package/dist/adapters/codemods/use-pnpm.js +214 -0
  30. package/dist/adapters/codemods/yaml.d.ts +2 -0
  31. package/dist/adapters/codemods/yaml.js +8 -0
  32. package/dist/adapters/commander/commands/codemod.d.ts +8 -0
  33. package/dist/adapters/commander/commands/codemod.js +22 -0
  34. package/dist/adapters/commander/commands/doctor.d.ts +4 -0
  35. package/dist/adapters/commander/commands/doctor.js +12 -0
  36. package/dist/adapters/commander/commands/info.d.ts +3 -0
  37. package/dist/adapters/commander/commands/info.js +9 -0
  38. package/dist/adapters/commander/commands/init.d.ts +7 -0
  39. package/dist/adapters/commander/commands/init.js +126 -0
  40. package/dist/adapters/commander/commands/savemoney.d.ts +2 -0
  41. package/dist/adapters/commander/commands/savemoney.js +26 -0
  42. package/dist/adapters/commander/index.d.ts +7 -0
  43. package/dist/adapters/commander/index.js +19 -0
  44. package/dist/adapters/execa/terraform.d.ts +27 -0
  45. package/dist/adapters/execa/terraform.js +28 -0
  46. package/dist/adapters/github/__tests__/github-repo.spec.d.ts +1 -0
  47. package/dist/adapters/github/__tests__/github-repo.spec.js +67 -0
  48. package/dist/adapters/github/github-repo.d.ts +2 -0
  49. package/dist/adapters/github/github-repo.js +31 -0
  50. package/dist/adapters/logtape/validation-reporter.d.ts +2 -0
  51. package/dist/adapters/logtape/validation-reporter.js +14 -0
  52. package/dist/adapters/node/__tests__/data.d.ts +18 -0
  53. package/dist/adapters/node/__tests__/data.js +22 -0
  54. package/dist/adapters/node/__tests__/package-json.test.d.ts +1 -0
  55. package/dist/adapters/node/__tests__/package-json.test.js +86 -0
  56. package/dist/adapters/node/__tests__/repository.test.d.ts +1 -0
  57. package/dist/adapters/node/__tests__/repository.test.js +77 -0
  58. package/dist/adapters/node/fs/__tests__/file-reader.test.d.ts +1 -0
  59. package/dist/adapters/node/fs/__tests__/file-reader.test.js +80 -0
  60. package/dist/adapters/node/fs/file-reader.d.ts +24 -0
  61. package/dist/adapters/node/fs/file-reader.js +26 -0
  62. package/dist/adapters/node/json/__tests__/index.test.d.ts +1 -0
  63. package/dist/adapters/node/json/__tests__/index.test.js +14 -0
  64. package/dist/adapters/node/json/index.d.ts +2 -0
  65. package/dist/adapters/node/json/index.js +2 -0
  66. package/dist/adapters/node/package-json.d.ts +2 -0
  67. package/dist/adapters/node/package-json.js +22 -0
  68. package/dist/adapters/node/release.d.ts +2 -0
  69. package/dist/adapters/node/release.js +33 -0
  70. package/dist/adapters/node/repository.d.ts +2 -0
  71. package/dist/adapters/node/repository.js +47 -0
  72. package/dist/adapters/octokit/__tests__/index.test.d.ts +1 -0
  73. package/dist/adapters/octokit/__tests__/index.test.js +197 -0
  74. package/dist/adapters/octokit/index.d.ts +24 -0
  75. package/dist/adapters/octokit/index.js +65 -0
  76. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.d.ts +1 -0
  77. package/dist/adapters/plop/actions/__tests__/init-cloud-accounts.test.js +115 -0
  78. package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.d.ts +1 -0
  79. package/dist/adapters/plop/actions/__tests__/provision-terraform-backend.test.js +116 -0
  80. package/dist/adapters/plop/actions/fetch-github-release.d.ts +12 -0
  81. package/dist/adapters/plop/actions/fetch-github-release.js +20 -0
  82. package/dist/adapters/plop/actions/get-node-version.d.ts +2 -0
  83. package/dist/adapters/plop/actions/get-node-version.js +9 -0
  84. package/dist/adapters/plop/actions/get-terraform-backend.d.ts +3 -0
  85. package/dist/adapters/plop/actions/get-terraform-backend.js +17 -0
  86. package/dist/adapters/plop/actions/init-cloud-accounts.d.ts +5 -0
  87. package/dist/adapters/plop/actions/init-cloud-accounts.js +13 -0
  88. package/dist/adapters/plop/actions/provision-terraform-backend.d.ts +10 -0
  89. package/dist/adapters/plop/actions/provision-terraform-backend.js +16 -0
  90. package/dist/adapters/plop/actions/semver.d.ts +19 -0
  91. package/dist/adapters/plop/actions/semver.js +27 -0
  92. package/dist/adapters/plop/actions/setup-pnpm.d.ts +2 -0
  93. package/dist/adapters/plop/actions/setup-pnpm.js +26 -0
  94. package/dist/adapters/plop/generators/environment/__tests__/actions.test.d.ts +2 -0
  95. package/dist/adapters/plop/generators/environment/__tests__/actions.test.js +55 -0
  96. package/dist/adapters/plop/generators/environment/actions.d.ts +2 -0
  97. package/dist/adapters/plop/generators/environment/actions.js +54 -0
  98. package/dist/adapters/plop/generators/environment/index.d.ts +3 -0
  99. package/dist/adapters/plop/generators/environment/index.js +19 -0
  100. package/dist/adapters/plop/generators/environment/prompts.d.ts +66 -0
  101. package/dist/adapters/plop/generators/environment/prompts.js +166 -0
  102. package/dist/adapters/plop/generators/monorepo/actions.d.ts +51 -0
  103. package/dist/adapters/plop/generators/monorepo/actions.js +35 -0
  104. package/dist/adapters/plop/generators/monorepo/index.d.ts +6 -0
  105. package/dist/adapters/plop/generators/monorepo/index.js +17 -0
  106. package/dist/adapters/plop/generators/monorepo/prompts.d.ts +10 -0
  107. package/dist/adapters/plop/generators/monorepo/prompts.js +31 -0
  108. package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.d.ts +1 -0
  109. package/dist/adapters/plop/helpers/__tests__/resource-prefix.test.js +113 -0
  110. package/dist/adapters/plop/helpers/env-short.d.ts +3 -0
  111. package/dist/adapters/plop/helpers/env-short.js +9 -0
  112. package/dist/adapters/plop/helpers/resource-prefix.d.ts +5 -0
  113. package/dist/adapters/plop/helpers/resource-prefix.js +18 -0
  114. package/dist/adapters/plop/index.d.ts +8 -0
  115. package/dist/adapters/plop/index.js +24 -0
  116. package/dist/adapters/terraform/fmt.d.ts +1 -0
  117. package/dist/adapters/terraform/fmt.js +17 -0
  118. package/dist/adapters/yaml/__tests__/index.test.d.ts +1 -0
  119. package/dist/adapters/yaml/__tests__/index.test.js +53 -0
  120. package/dist/adapters/yaml/index.d.ts +8 -0
  121. package/dist/adapters/yaml/index.js +9 -0
  122. package/dist/adapters/zod/index.d.ts +23 -0
  123. package/dist/adapters/zod/index.js +22 -0
  124. package/dist/config.d.ts +6 -0
  125. package/dist/config.js +5 -0
  126. package/dist/domain/__tests__/data.d.ts +17 -0
  127. package/dist/domain/__tests__/data.js +27 -0
  128. package/dist/domain/__tests__/environment.test.d.ts +1 -0
  129. package/dist/domain/__tests__/environment.test.js +282 -0
  130. package/dist/domain/__tests__/info.test.d.ts +1 -0
  131. package/dist/domain/__tests__/info.test.js +77 -0
  132. package/dist/domain/__tests__/package-json.test.d.ts +1 -0
  133. package/dist/domain/__tests__/package-json.test.js +39 -0
  134. package/dist/domain/__tests__/repository.test.d.ts +1 -0
  135. package/dist/domain/__tests__/repository.test.js +101 -0
  136. package/dist/domain/__tests__/workspace.test.d.ts +1 -0
  137. package/dist/domain/__tests__/workspace.test.js +57 -0
  138. package/dist/domain/cloud-account.d.ts +28 -0
  139. package/dist/domain/cloud-account.js +12 -0
  140. package/dist/domain/codemod.d.ts +11 -0
  141. package/dist/domain/codemod.js +1 -0
  142. package/dist/domain/dependencies.d.ts +10 -0
  143. package/dist/domain/dependencies.js +1 -0
  144. package/dist/domain/doctor.d.ts +10 -0
  145. package/dist/domain/doctor.js +50 -0
  146. package/dist/domain/environment.d.ts +40 -0
  147. package/dist/domain/environment.js +57 -0
  148. package/dist/domain/github-repo.d.ts +6 -0
  149. package/dist/domain/github-repo.js +8 -0
  150. package/dist/domain/github.d.ts +37 -0
  151. package/dist/domain/github.js +29 -0
  152. package/dist/domain/info.d.ts +11 -0
  153. package/dist/domain/info.js +52 -0
  154. package/dist/domain/package-json.d.ts +42 -0
  155. package/dist/domain/package-json.js +69 -0
  156. package/dist/domain/remote-backend.d.ts +8 -0
  157. package/dist/domain/remote-backend.js +9 -0
  158. package/dist/domain/repository.d.ts +13 -0
  159. package/dist/domain/repository.js +72 -0
  160. package/dist/domain/validation.d.ts +16 -0
  161. package/dist/domain/validation.js +1 -0
  162. package/dist/domain/workspace.d.ts +9 -0
  163. package/dist/domain/workspace.js +32 -0
  164. package/dist/index.d.ts +2 -0
  165. package/dist/index.js +35 -0
  166. package/dist/use-cases/__tests__/apply-codemod.test.d.ts +1 -0
  167. package/dist/use-cases/__tests__/apply-codemod.test.js +73 -0
  168. package/dist/use-cases/__tests__/list-codemods.test.d.ts +1 -0
  169. package/dist/use-cases/__tests__/list-codemods.test.js +38 -0
  170. package/dist/use-cases/apply-codemod.d.ts +5 -0
  171. package/dist/use-cases/apply-codemod.js +14 -0
  172. package/dist/use-cases/list-codemods.d.ts +4 -0
  173. package/dist/use-cases/list-codemods.js +1 -0
  174. package/package.json +19 -8
  175. package/templates/environment/bootstrapper/{{env.name}}/data.tf.hbs +13 -0
  176. package/templates/environment/bootstrapper/{{env.name}}/main.tf.hbs +70 -0
  177. package/templates/environment/bootstrapper/{{env.name}}/providers.tf.hbs +26 -0
  178. package/templates/environment/core/{{env.name}}/main.tf.hbs +21 -0
  179. package/templates/environment/core/{{env.name}}/outputs.tf.hbs +8 -0
  180. package/templates/environment/core/{{env.name}}/providers.tf.hbs +21 -0
  181. package/templates/environment/shared/backend.tf.hbs +14 -0
  182. package/templates/environment/shared/locals.tf.hbs +26 -0
  183. package/templates/monorepo/.editorconfig +8 -0
  184. package/templates/monorepo/.node-version.hbs +1 -0
  185. package/templates/monorepo/.pre-commit-config.yaml.hbs +34 -0
  186. package/templates/monorepo/.prettierignore +5 -0
  187. package/templates/monorepo/.terraform-version.hbs +1 -0
  188. package/templates/monorepo/.trivyignore +16 -0
  189. package/templates/monorepo/README.md.hbs +163 -0
  190. package/templates/monorepo/infra/repository/main.tf.hbs +11 -0
  191. package/templates/monorepo/infra/repository/outputs.tf.hbs +11 -0
  192. package/templates/monorepo/infra/repository/providers.tf.hbs +13 -0
  193. package/templates/monorepo/package.json.hbs +7 -0
  194. package/templates/monorepo/pnpm-workspace.yaml +7 -0
  195. package/templates/monorepo/turbo.json +29 -0
@@ -0,0 +1,214 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { $ } from "execa";
3
+ import assert from "node:assert/strict";
4
+ import fs from "node:fs/promises";
5
+ import { replaceInFile } from "replace-in-file";
6
+ import semver from "semver";
7
+ import YAML from "yaml";
8
+ import { getLatestCommitShaOrRef } from "./git.js";
9
+ import { updateJSCodeReviewJob } from "./update-code-review.js";
10
+ import { migrateWorkflow } from "./use-azure-appsvc.js";
11
+ export class NPM {
12
+ lockFileName = "package-lock.json";
13
+ async listWorkspaces() {
14
+ const { stdout } = await $ `npm query .workspace`;
15
+ const workspaces = JSON.parse(stdout);
16
+ const workspaceNames = [];
17
+ if (Array.isArray(workspaces)) {
18
+ for (const ws of workspaces) {
19
+ if (Object.hasOwn(ws, "name")) {
20
+ workspaceNames.push(ws.name);
21
+ }
22
+ }
23
+ }
24
+ return workspaceNames;
25
+ }
26
+ }
27
+ export class Yarn {
28
+ lockFileName = "yarn.lock";
29
+ async listWorkspaces() {
30
+ const { stdout } = await $({ lines: true }) `yarn workspaces list --json`;
31
+ const workspaceNames = [];
32
+ for (const line of stdout) {
33
+ const ws = JSON.parse(line);
34
+ if (Object.hasOwn(ws, "name")) {
35
+ workspaceNames.push(ws.name);
36
+ }
37
+ }
38
+ return workspaceNames;
39
+ }
40
+ }
41
+ export async function extractPackageExtensions() {
42
+ // Read the .yarnrc.yaml file if it exists and extract the packageExtensions field
43
+ try {
44
+ const yarnrc = await fs.readFile(".yarnrc.yml", "utf-8");
45
+ const parsed = YAML.parse(yarnrc);
46
+ if (parsed.packageExtensions) {
47
+ return parsed.packageExtensions;
48
+ }
49
+ }
50
+ catch {
51
+ // File does not exist or is not readable, ignore
52
+ }
53
+ return undefined;
54
+ }
55
+ export async function preparePackageJsonForPnpm() {
56
+ const packageJson = await fs.readFile("package.json", "utf-8");
57
+ const manifest = JSON.parse(packageJson);
58
+ let workspaces = [];
59
+ if (Object.hasOwn(manifest, "packageManager")) {
60
+ delete manifest.packageManager;
61
+ }
62
+ if (Object.hasOwn(manifest, "workspaces")) {
63
+ if (Array.isArray(manifest.workspaces)) {
64
+ workspaces = manifest.workspaces;
65
+ }
66
+ delete manifest.workspaces;
67
+ }
68
+ await fs.writeFile("package.json", JSON.stringify(manifest, null, 2));
69
+ return workspaces;
70
+ }
71
+ export async function writePnpmWorkspaceFile(workspaces, packageExtensions) {
72
+ // We inline all the default settings here because Renovate
73
+ // does not support PNPM's config dependencies yet.
74
+ const pnpmWorkspace = {
75
+ cleanupUnusedCatalogs: true,
76
+ linkWorkspacePackages: true,
77
+ packageExtensions,
78
+ packageImportMethod: "clone-or-copy",
79
+ packages: workspaces.length > 0 ? workspaces : ["apps/*", "packages/*"],
80
+ };
81
+ const yamlContent = YAML.stringify(pnpmWorkspace);
82
+ await fs.writeFile("pnpm-workspace.yaml", yamlContent, "utf-8");
83
+ }
84
+ async function removeFiles(...files) {
85
+ await Promise.all(files.map((file) =>
86
+ // Remove the file if it exists, fail silently if it doesn't.
87
+ fs.rm(file, { force: true, recursive: true }).catch(() => undefined)));
88
+ }
89
+ async function replacePMOccurrences() {
90
+ const logger = getLogger(["dx-cli", "codemod"]);
91
+ logger.info("Replacing yarn and npm occurrences in files...");
92
+ const results = await replaceInFile({
93
+ allowEmptyPaths: true,
94
+ files: ["**/*.json", "**/*.md", "**/Dockerfile", "**/docker-compose.yml"],
95
+ from: [
96
+ "https://yarnpkg.com/",
97
+ "https://classic.yarnpkg.com/",
98
+ /\b(yarn workspace|npm -(\b-workspace\b|\bw\b)) (\S+)\b/g,
99
+ /\b(yarn workspace|npm -(\b-workspace\b|\bw\b))\b/g,
100
+ /\b(yarn install --immutable|npm ci)\b/g,
101
+ /\b(yarn -q dlx|npx)\b/g,
102
+ /\b((yarn|npm) run)\b/g,
103
+ /(^|\s|")(yarn|npm)(?!\S)/gi,
104
+ ],
105
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
106
+ to: [
107
+ "https://pnpm.io/",
108
+ "https://pnpm.io/",
109
+ "pnpm --filter $3",
110
+ "pnpm --filter <package-selector>",
111
+ "pnpm install --frozen-lockfile",
112
+ "pnpm dlx",
113
+ "pnpm run",
114
+ "$1pnpm",
115
+ ],
116
+ });
117
+ const count = results.reduce((acc, file) => (file.hasChanged ? acc + 1 : acc), 0);
118
+ logger.info("Replaced yarn occurrences in {count} files", { count });
119
+ }
120
+ async function updateDXWorkflows() {
121
+ const logger = getLogger(["dx-cli", "codemod"]);
122
+ logger.info("Updating Github Workflows workflows...");
123
+ // Get the latest commit sha from the main branch of the dx repository
124
+ const sha = await getLatestCommitShaOrRef("pagopa", "dx");
125
+ // Update the js_code_review workflow to use the latest commit sha
126
+ const results = await replaceInFile({
127
+ allowEmptyPaths: true,
128
+ files: [".github/workflows/*.yaml"],
129
+ processor: updateJSCodeReviewJob(sha),
130
+ });
131
+ const ignore = results.filter((r) => r.hasChanged).map((r) => r.file);
132
+ // Update the legacy deployment workflow to release-azure-appsvc-v1.yaml
133
+ await replaceInFile({
134
+ allowEmptyPaths: true,
135
+ files: [".github/workflows/*.yaml"],
136
+ ignore,
137
+ processor: migrateWorkflow(sha),
138
+ });
139
+ }
140
+ export const usePnpm = async (packageManager, currentNodeVersion) => {
141
+ const minNodeVersion = "20.19.5";
142
+ assert.notEqual(packageManager, "pnpm", "Project is already using pnpm");
143
+ assert.ok(semver.gte(currentNodeVersion, minNodeVersion), `his codemod requires Node.js >= ${minNodeVersion}. Current version: ${currentNodeVersion}`);
144
+ const logger = getLogger(["dx-cli", "codemod"]);
145
+ const pm = packageManager === "yarn" ? new Yarn() : new NPM();
146
+ const localWorkspaces = await pm.listWorkspaces();
147
+ // Update local dependencies to use "workspace:" protocol
148
+ if (localWorkspaces.length > 0) {
149
+ logger.info("Using the {protocol} protocol for local dependencies", {
150
+ protocol: "workspace:",
151
+ });
152
+ await replaceInFile({
153
+ allowEmptyPaths: true,
154
+ files: ["**/package.json"],
155
+ from: localWorkspaces.map((ws) => new RegExp(`"${ws}": ".*?"`, "g")),
156
+ to: localWorkspaces.map((ws) => `"${ws}": "workspace:^"`),
157
+ });
158
+ }
159
+ // Remove unused field from package.json
160
+ logger.info("Remove unused fields from {file}", {
161
+ file: "package.json",
162
+ });
163
+ const workspaces = await preparePackageJsonForPnpm();
164
+ // Extract custom packageExtensions from .yarnrc.yml if any
165
+ const packageExtensions = packageManager === "yarn" ? await extractPackageExtensions() : undefined;
166
+ // Create pnpm-workspace.yaml
167
+ logger.info("Create {file}", {
168
+ file: "pnpm-workspace.yaml",
169
+ });
170
+ await writePnpmWorkspaceFile(workspaces, packageExtensions);
171
+ // Remove yarn and node_modules files and folders
172
+ logger.info("Remove node_modules and yarn files");
173
+ await removeFiles(".yarnrc", ".yarnrc.yml", "yarn.config.cjs", ".yarn", ".pnp.cjs", ".pnp.loader.cjs", "node_modules");
174
+ // Import lockfile
175
+ const stat = await fs.stat(pm.lockFileName);
176
+ if (stat.isFile()) {
177
+ logger.info("Importing {source} to {target}", {
178
+ source: pm.lockFileName,
179
+ target: "pnpm-lock.yaml",
180
+ });
181
+ await $ `corepack pnpm@latest import ${pm.lockFileName}`;
182
+ await removeFiles(pm.lockFileName);
183
+ }
184
+ else {
185
+ logger.info("No {source} file found, skipping import.", {
186
+ source: pm.lockFileName,
187
+ });
188
+ }
189
+ // Replace yarn and npm occurrences in files and update workflows
190
+ await replacePMOccurrences();
191
+ await updateDXWorkflows();
192
+ // Add pnpm store to .gitignore
193
+ logger.info("Adding pnpm store to .gitignore...");
194
+ await fs.appendFile(".gitignore", "\n\n# PNPM\n.pnpm-store");
195
+ // Set pnpm as the package manager
196
+ logger.info("Setting pnpm as the package manager...");
197
+ await $ `corepack use pnpm@latest`;
198
+ };
199
+ const apply = async (info) => {
200
+ const logger = getLogger(["dx-cli", "codemod"]);
201
+ try {
202
+ await usePnpm(info.packageManager, process.versions.node);
203
+ }
204
+ catch (error) {
205
+ if (error instanceof Error) {
206
+ logger.error(error.message);
207
+ }
208
+ }
209
+ };
210
+ export default {
211
+ apply,
212
+ description: "Migrate the project to use pnpm as the package manager",
213
+ id: "use-pnpm",
214
+ };
@@ -0,0 +1,2 @@
1
+ import * as YAML from "yaml";
2
+ export declare const isChildOf: (path: Parameters<YAML.visitorFn<unknown>>[2], key: string) => boolean;
@@ -0,0 +1,8 @@
1
+ import * as YAML from "yaml";
2
+ export const isChildOf = (path, key) => {
3
+ const ancestor = path.at(-1);
4
+ return (YAML.isPair(ancestor) &&
5
+ YAML.isScalar(ancestor.key) &&
6
+ typeof ancestor.key.value === "string" &&
7
+ ancestor.key.value === key);
8
+ };
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { ApplyCodemodById } from "../../../use-cases/apply-codemod.js";
3
+ import { ListCodemods } from "../../../use-cases/list-codemods.js";
4
+ export type CodemodCommandDependencies = {
5
+ applyCodemodById: ApplyCodemodById;
6
+ listCodemods: ListCodemods;
7
+ };
8
+ export declare const makeCodemodCommand: ({ applyCodemodById, listCodemods, }: CodemodCommandDependencies) => Command;
@@ -0,0 +1,22 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ import { Command } from "commander";
3
+ export const makeCodemodCommand = ({ applyCodemodById, listCodemods, }) => new Command("codemod")
4
+ .description("Manage and apply migration scripts to the repository")
5
+ .addCommand(new Command("list")
6
+ .description("List available migration scripts")
7
+ .action(async function () {
8
+ await listCodemods()
9
+ .andTee((codemods) => console.table(codemods, ["id", "description"]))
10
+ .orTee((error) => this.error(error.message));
11
+ }))
12
+ .addCommand(new Command("apply")
13
+ .argument("<id>", "The id of the codemod to apply")
14
+ .description("Apply migration scripts to the repository")
15
+ .action(async function (id) {
16
+ const logger = getLogger(["dx-cli", "codemod"]);
17
+ await applyCodemodById(id)
18
+ .andTee(() => {
19
+ logger.info("Codemod applied ✅");
20
+ })
21
+ .orTee((error) => this.error(error.message));
22
+ }));
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import { Config } from "../../../config.js";
3
+ import { Dependencies } from "../../../domain/dependencies.js";
4
+ export declare const makeDoctorCommand: (dependencies: Dependencies, config: Config) => Command;
@@ -0,0 +1,12 @@
1
+ import { Command } from "commander";
2
+ import * as process from "node:process";
3
+ import { printDoctorResult, runDoctor } from "../../../domain/doctor.js";
4
+ export const makeDoctorCommand = (dependencies, config) => new Command()
5
+ .name("doctor")
6
+ .description("Verify the repository setup according to the DevEx guidelines")
7
+ .action(async () => {
8
+ const result = await runDoctor(dependencies, config);
9
+ printDoctorResult(dependencies, result);
10
+ const exitCode = result.hasErrors ? 1 : 0;
11
+ process.exit(exitCode);
12
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import { Dependencies } from "../../../domain/dependencies.js";
3
+ export declare const makeInfoCommand: (dependencies: Dependencies) => Command;
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import { getInfo, printInfo } from "../../../domain/info.js";
3
+ export const makeInfoCommand = (dependencies) => new Command()
4
+ .name("info")
5
+ .description("Display information about the project")
6
+ .action(async () => {
7
+ const result = await getInfo(dependencies)();
8
+ printInfo(result);
9
+ });
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import { GitHubService } from "../../../domain/github.js";
3
+ type InitCommandDependencies = {
4
+ gitHubService: GitHubService;
5
+ };
6
+ export declare const makeInitCommand: ({ gitHubService, }: InitCommandDependencies) => Command;
7
+ export {};
@@ -0,0 +1,126 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { $ } from "execa";
4
+ import { errAsync, okAsync, ResultAsync } from "neverthrow";
5
+ import * as path from "node:path";
6
+ import { oraPromise } from "ora";
7
+ import { Repository, RepositoryNotFoundError, } from "../../../domain/github.js";
8
+ import { tf$ } from "../../execa/terraform.js";
9
+ import { payloadSchema as answersSchema, PLOP_MONOREPO_GENERATOR_NAME, } from "../../plop/generators/monorepo/index.js";
10
+ import { setMonorepoGenerator } from "../../plop/index.js";
11
+ import { getGenerator, getPrompts, initPlop } from "../../plop/index.js";
12
+ import { decode } from "../../zod/index.js";
13
+ import { exitWithError } from "../index.js";
14
+ const withSpinner = (text, successText, failText, promise) => ResultAsync.fromPromise(oraPromise(promise, {
15
+ failText,
16
+ successText,
17
+ text,
18
+ }), (cause) => {
19
+ console.error(`Something went wrong: ${JSON.stringify(cause, null, 2)}`);
20
+ return new Error(failText, { cause });
21
+ });
22
+ // TODO: Check Cloud Environment exists
23
+ // TODO: Check CSP CLI is installed
24
+ // TODO: Check user has permissions to handle Terraform state
25
+ const validateAnswers = (githubService) => (answers) => ResultAsync.fromPromise(githubService.getRepository(answers.repoOwner, answers.repoName), (error) => error)
26
+ .andThen(({ fullName }) => errAsync(new Error(`Repository ${fullName} already exists.`)))
27
+ .orElse((error) => error instanceof RepositoryNotFoundError
28
+ ? // If repository is not found, it's safe to proceed
29
+ okAsync(answers)
30
+ : // Otherwise, propagate the error
31
+ errAsync(error))
32
+ .map(() => answers);
33
+ const runGeneratorActions = (generator) => (answers) => withSpinner("Creating workspace files...", "Workspace files created successfully!", "Failed to create workspace files.", generator.runActions(answers)).map(() => answers);
34
+ const displaySummary = (initResult) => {
35
+ const { pr, repository } = initResult;
36
+ console.log(chalk.green.bold("\nWorkspace created successfully!"));
37
+ if (repository) {
38
+ console.log(`- Name: ${chalk.cyan(repository.name)}`);
39
+ console.log(`- GitHub Repository: ${chalk.cyan(repository.url)}\n`);
40
+ }
41
+ else {
42
+ console.log(chalk.yellow(`\n⚠️ GitHub repository may not have been created automatically.`));
43
+ }
44
+ if (pr) {
45
+ console.log(chalk.green.bold("\nNext Steps:"));
46
+ console.log(`1. Review the Pull Request in the GitHub repository: ${chalk.underline(pr.url)}`);
47
+ console.log(`2. Visit ${chalk.underline("https://dx.pagopa.it/getting-started")} to deploy your first project\n`);
48
+ }
49
+ else {
50
+ console.log(chalk.yellow(`\n⚠️ There was an error during Pull Request creation.`));
51
+ console.log(`Please, manually create a Pull Request in the GitHub repository to review the scaffolded code.\n`);
52
+ }
53
+ };
54
+ const checkTerraformCliIsInstalled = (text, successText, failText) => withSpinner(text, successText, failText, tf$ `terraform -version`);
55
+ const checkPreconditions = () => checkTerraformCliIsInstalled("Checking Terraform CLI is installed...", "Terraform CLI is installed!", "Terraform CLI is not installed.");
56
+ const createRemoteRepository = ({ repoName, repoOwner, }) => {
57
+ const cwd = path.resolve(repoName, "infra", "repository");
58
+ const applyTerraform = async () => {
59
+ await tf$({ cwd }) `terraform init`;
60
+ await tf$({ cwd }) `terraform apply -auto-approve`;
61
+ };
62
+ return withSpinner("Creating GitHub repository...", "GitHub repository created successfully!", "Failed to create GitHub repository.", applyTerraform()).map(() => new Repository(repoName, repoOwner));
63
+ };
64
+ const initializeGitRepository = (repository) => {
65
+ const cwd = path.resolve(repository.name);
66
+ const branchName = "features/scaffold-workspace";
67
+ const git$ = $({
68
+ cwd,
69
+ shell: true,
70
+ });
71
+ const pushToOrigin = async () => {
72
+ await git$ `git init`;
73
+ await git$ `git add README.md`;
74
+ await git$ `git commit --no-gpg-sign -m "Create README.md"`;
75
+ await git$ `git branch -M main`;
76
+ await git$ `git remote add origin ${repository.ssh}`;
77
+ await git$ `git push -u origin main`;
78
+ await git$ `git switch -c ${branchName}`;
79
+ await git$ `git add .`;
80
+ await git$ `git commit --no-gpg-sign -m "Scaffold workspace"`;
81
+ await git$ `git push -u origin ${branchName}`;
82
+ };
83
+ return withSpinner("Pushing code to GitHub...", "Code pushed to GitHub successfully!", "Failed to push code to GitHub.", pushToOrigin()).map(() => ({ branchName, repository }));
84
+ };
85
+ const handleNewGitHubRepository = (githubService) => (answers) => createRemoteRepository(answers)
86
+ .andThen(initializeGitRepository)
87
+ .andThen((localWorkspace) => createPullRequest(githubService)(localWorkspace).map((pr) => ({
88
+ pr,
89
+ repository: localWorkspace.repository,
90
+ })));
91
+ const makeInitResult = (answers, { pr, repository }) => ({
92
+ pr,
93
+ repository,
94
+ });
95
+ const createPullRequest = (githubService) => ({ branchName, repository, }) => withSpinner("Creating Pull Request...", "Pull Request created successfully!", "Failed to create Pull Request.", githubService.createPullRequest({
96
+ base: "main",
97
+ body: "This PR contains the scaffolded monorepo structure.",
98
+ head: branchName,
99
+ owner: repository.owner,
100
+ repo: repository.name,
101
+ title: "Scaffold repository",
102
+ }))
103
+ // If PR creation fails, don't block the workflow
104
+ .orElse(() => okAsync(undefined));
105
+ export const makeInitCommand = ({ gitHubService, }) => new Command()
106
+ .name("init")
107
+ .description("Command to initialize resources (like projects, subscriptions, ...)")
108
+ .addCommand(new Command("project")
109
+ .description("Initialize a new monorepo project")
110
+ .action(async function () {
111
+ await checkPreconditions()
112
+ .andThen(initPlop)
113
+ .andTee(setMonorepoGenerator)
114
+ .andThen((plop) => getGenerator(plop)(PLOP_MONOREPO_GENERATOR_NAME))
115
+ .andThen((generator) =>
116
+ // Ask the user the questions defined in the plop generator
117
+ getPrompts(generator)
118
+ // Decode the answers to match the Answers schema
119
+ .andThen(decode(answersSchema))
120
+ // Validate the answers (like checking permissions, checking GitHub user or org existence, etc.)
121
+ .andThen(validateAnswers(gitHubService))
122
+ // Run the generator with the provided answers (this will create the files locally)
123
+ .andThen(runGeneratorActions(generator)))
124
+ .andThen((answers) => handleNewGitHubRepository(gitHubService)(answers).map((repoPr) => makeInitResult(answers, repoPr)))
125
+ .match(displaySummary, exitWithError(this));
126
+ }));
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const makeSavemoneyCommand: () => Command;
@@ -0,0 +1,26 @@
1
+ import { azure, loadConfig } from "@pagopa/dx-savemoney";
2
+ import { Command } from "commander";
3
+ export const makeSavemoneyCommand = () => new Command("savemoney")
4
+ .description("Analyze Azure subscriptions and report unused or inefficient resources")
5
+ .option("-c, --config <path>", "Path to configuration file (JSON)")
6
+ .option("-f, --format <format>", "Report format: json, table, or detailed-json (default: table)", "table")
7
+ .option("-l, --location <string>", "Preferred Azure location for resources", "italynorth")
8
+ .option("-d, --days <number>", "Number of days for metrics analysis", "30")
9
+ .option("-v, --verbose", "Enable verbose logging")
10
+ .action(async function (options) {
11
+ try {
12
+ // Load configuration
13
+ const config = await loadConfig(options.config);
14
+ const finalConfig = {
15
+ ...config,
16
+ preferredLocation: options.location || config.preferredLocation,
17
+ timespanDays: Number.parseInt(options.days, 10) || config.timespanDays,
18
+ verbose: options.verbose || false,
19
+ };
20
+ // Run analysis
21
+ await azure.analyzeAzureResources(finalConfig, options.format);
22
+ }
23
+ catch (error) {
24
+ this.error(`Analysis failed: ${error instanceof Error ? error.message : error}`);
25
+ }
26
+ });
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import { Config } from "../../config.js";
3
+ import { Dependencies } from "../../domain/dependencies.js";
4
+ import { CodemodCommandDependencies } from "./commands/codemod.js";
5
+ export type CliDependencies = CodemodCommandDependencies;
6
+ export declare const makeCli: (deps: Dependencies, config: Config, cliDeps: CliDependencies, version: string) => Command;
7
+ export declare const exitWithError: (command: Command) => (error: Error) => never;
@@ -0,0 +1,19 @@
1
+ import { Command } from "commander";
2
+ import { makeCodemodCommand, } from "./commands/codemod.js";
3
+ import { makeDoctorCommand } from "./commands/doctor.js";
4
+ import { makeInfoCommand } from "./commands/info.js";
5
+ import { makeInitCommand } from "./commands/init.js";
6
+ import { makeSavemoneyCommand } from "./commands/savemoney.js";
7
+ export const makeCli = (deps, config, cliDeps, version) => {
8
+ const program = new Command();
9
+ program.name("dx").description("The CLI for DX-Platform").version(version);
10
+ program.addCommand(makeDoctorCommand(deps, config));
11
+ program.addCommand(makeCodemodCommand(cliDeps));
12
+ program.addCommand(makeInitCommand(deps));
13
+ program.addCommand(makeSavemoneyCommand());
14
+ program.addCommand(makeInfoCommand(deps));
15
+ return program;
16
+ };
17
+ export const exitWithError = (command) => (error) => {
18
+ command.error(error.message);
19
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A pre-configured instance of the `execa` library for running Terraform commands.
3
+ *
4
+ * This instance is customized with specific environment variables and shell settings
5
+ * to ensure compatibility with Terraform automation workflows.
6
+ *
7
+ * Environment Variables:
8
+ * - `NO_COLOR`: Disables colored output to make logs cleaner and easier to parse.
9
+ * - `TF_IN_AUTOMATION`: Indicates that Terraform is running in an automated environment.
10
+ * - `TF_INPUT`: Disables interactive prompts, ensuring non-interactive execution.
11
+ *
12
+ * Configuration:
13
+ * - `shell: true`: Enables the use of the system shell for command execution.
14
+ *
15
+ * Usage:
16
+ * @example
17
+ * await tf$`terraform init`;
18
+ * await tf$`terraform apply -auto-approve`;
19
+ */
20
+ export declare const tf$: import("execa").ExecaScriptMethod<{
21
+ environment: {
22
+ NO_COLOR: string;
23
+ TF_IN_AUTOMATION: string;
24
+ TF_INPUT: string;
25
+ };
26
+ shell: true;
27
+ }>;
@@ -0,0 +1,28 @@
1
+ import { $ } from "execa";
2
+ /**
3
+ * A pre-configured instance of the `execa` library for running Terraform commands.
4
+ *
5
+ * This instance is customized with specific environment variables and shell settings
6
+ * to ensure compatibility with Terraform automation workflows.
7
+ *
8
+ * Environment Variables:
9
+ * - `NO_COLOR`: Disables colored output to make logs cleaner and easier to parse.
10
+ * - `TF_IN_AUTOMATION`: Indicates that Terraform is running in an automated environment.
11
+ * - `TF_INPUT`: Disables interactive prompts, ensuring non-interactive execution.
12
+ *
13
+ * Configuration:
14
+ * - `shell: true`: Enables the use of the system shell for command execution.
15
+ *
16
+ * Usage:
17
+ * @example
18
+ * await tf$`terraform init`;
19
+ * await tf$`terraform apply -auto-approve`;
20
+ */
21
+ export const tf$ = $({
22
+ environment: {
23
+ NO_COLOR: "1",
24
+ TF_IN_AUTOMATION: "1",
25
+ TF_INPUT: "0",
26
+ },
27
+ shell: true,
28
+ });
@@ -0,0 +1,67 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ vi.mock("execa", () => ({
3
+ $: vi.fn(),
4
+ }));
5
+ import { $ } from "execa";
6
+ import { getGithubRepo } from "../github-repo.js";
7
+ const mock$ = $;
8
+ describe("getGithubRepo", () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+ it("should return undefined if no remote URL is set", async () => {
13
+ mock$.mockResolvedValue({ stdout: "" });
14
+ const result = await getGithubRepo();
15
+ expect(result).toBeUndefined();
16
+ });
17
+ it("should parse GitHub repository URL and return owner and repo", async () => {
18
+ mock$.mockResolvedValue({ stdout: "https://github.com/pagopa/dx" });
19
+ const result = await getGithubRepo();
20
+ expect(result).toEqual({
21
+ owner: "pagopa",
22
+ repo: "dx",
23
+ });
24
+ });
25
+ it("should handle repository URLs with .git suffix", async () => {
26
+ mock$.mockResolvedValue({ stdout: "https://github.com/pagopa/dx.git" });
27
+ const result = await getGithubRepo();
28
+ expect(result).toEqual({
29
+ owner: "pagopa",
30
+ repo: "dx",
31
+ });
32
+ });
33
+ it("should throw an error for non-GitHub repositories", async () => {
34
+ mock$.mockResolvedValue({ stdout: "https://gitlab.com/owner/repo" });
35
+ await expect(getGithubRepo()).rejects.toThrow("Only GitHub repositories are supported");
36
+ });
37
+ it("should handle repository names with hyphens", async () => {
38
+ mock$.mockResolvedValue({
39
+ stdout: "https://github.com/my-org/my-repo-name",
40
+ });
41
+ const result = await getGithubRepo();
42
+ expect(result).toEqual({
43
+ owner: "my-org",
44
+ repo: "my-repo-name",
45
+ });
46
+ });
47
+ it("should handle repository names with underscores", async () => {
48
+ mock$.mockResolvedValue({
49
+ stdout: "https://github.com/my_org/my_repo_name",
50
+ });
51
+ const result = await getGithubRepo();
52
+ expect(result).toEqual({
53
+ owner: "my_org",
54
+ repo: "my_repo_name",
55
+ });
56
+ });
57
+ it("should handle ssh repository URLs", async () => {
58
+ mock$.mockResolvedValue({
59
+ stdout: "git@github.com:my-org/my-repo-name.git",
60
+ });
61
+ const result = await getGithubRepo();
62
+ expect(result).toEqual({
63
+ owner: "my-org",
64
+ repo: "my-repo-name",
65
+ });
66
+ });
67
+ });
@@ -0,0 +1,2 @@
1
+ import { GitHubRepo } from "../../domain/github-repo.js";
2
+ export declare const getGithubRepo: () => Promise<GitHubRepo | undefined>;
@@ -0,0 +1,31 @@
1
+ import { $ } from "execa";
2
+ import * as assert from "node:assert/strict";
3
+ import { githubRepoSchema } from "../../domain/github-repo.js";
4
+ export const getGithubRepo = async () => {
5
+ const result = await $ `git config --get remote.origin.url`;
6
+ const repoUrl = result.stdout.trim();
7
+ if (repoUrl === "") {
8
+ return undefined;
9
+ }
10
+ let owner;
11
+ let repo;
12
+ // Handle SSH URLs (git@github.com:owner/repo.git)
13
+ if (repoUrl.startsWith("git@github.com:")) {
14
+ const sshPattern = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/;
15
+ const match = repoUrl.match(sshPattern);
16
+ assert.ok(match, "Invalid GitHub SSH URL format");
17
+ [, owner, repo] = match;
18
+ }
19
+ else {
20
+ // Handle HTTPS URLs (https://github.com/owner/repo.git)
21
+ const url = new URL(repoUrl);
22
+ assert.equal(url.origin, "https://github.com", "Only GitHub repositories are supported");
23
+ const [, urlOwner, urlRepo] = url.pathname.split("/");
24
+ owner = urlOwner;
25
+ repo = urlRepo.replace(/\.git$/, "");
26
+ }
27
+ return githubRepoSchema.parse({
28
+ owner,
29
+ repo,
30
+ });
31
+ };
@@ -0,0 +1,2 @@
1
+ import { ValidationReporter } from "../../domain/validation.js";
2
+ export declare const makeValidationReporter: () => ValidationReporter;