@pagopa/dx-cli 0.14.3 → 0.14.5

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 (3) hide show
  1. package/README.md +3 -0
  2. package/bin/index.js +144 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -122,10 +122,13 @@ This command will:
122
122
 
123
123
  - Check that required tools (e.g., Terraform CLI) are installed
124
124
  - Interactively prompt for project metadata (cloud provider, region, environments, cost center, etc.)
125
+ - **Check that the target GitHub repository does not already exist before proceeding**
125
126
  - Generate a monorepo structure following PagoPA DevEx guidelines
126
127
  - Create a remote GitHub repository using Terraform
127
128
  - Push the initial codebase to the newly created repository
128
129
 
130
+ If the specified GitHub repository already exists, the command will fail early with a clear error message, preventing accidental overwrites.
131
+
129
132
  **Example usage:**
130
133
 
131
134
  ```bash
package/bin/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import "core-js/actual/set/index.js";
5
5
  import { configure, getConsoleSink } from "@logtape/logtape";
6
- import { Octokit as Octokit2 } from "octokit";
6
+ import { Octokit as Octokit3 } from "octokit";
7
7
 
8
8
  // src/adapters/codemods/registry.ts
9
9
  import { okAsync } from "neverthrow";
@@ -201,7 +201,8 @@ var useAzureAppsvc = {
201
201
  // src/adapters/codemods/use-pnpm.ts
202
202
  import { getLogger as getLogger4 } from "@logtape/logtape";
203
203
  import { $ } from "execa";
204
- import * as fs from "fs/promises";
204
+ import assert from "assert/strict";
205
+ import fs from "fs/promises";
205
206
  import { replaceInFile as replaceInFile3 } from "replace-in-file";
206
207
  import semver from "semver";
207
208
  import YAML4 from "yaml";
@@ -262,6 +263,17 @@ async function preparePackageJsonForPnpm() {
262
263
  await fs.writeFile("package.json", JSON.stringify(manifest, null, 2));
263
264
  return workspaces;
264
265
  }
266
+ async function writePnpmWorkspaceFile(workspaces, packageExtensions) {
267
+ const pnpmWorkspace = {
268
+ cleanupUnusedCatalogs: true,
269
+ linkWorkspacePackages: true,
270
+ packageExtensions,
271
+ packageImportMethod: "clone-or-copy",
272
+ packages: workspaces.length > 0 ? workspaces : ["apps/*", "packages/*"]
273
+ };
274
+ const yamlContent = YAML4.stringify(pnpmWorkspace);
275
+ await fs.writeFile("pnpm-workspace.yaml", yamlContent, "utf-8");
276
+ }
265
277
  async function removeFiles(...files) {
266
278
  await Promise.all(
267
279
  files.map(
@@ -285,7 +297,7 @@ async function replacePMOccurrences() {
285
297
  /\b(yarn workspace|npm -(\b-workspace\b|\bw\b))\b/g,
286
298
  /\b(yarn install --immutable|npm ci)\b/g,
287
299
  /\b(yarn -q dlx|npx)\b/g,
288
- /\b(Yarn|npm)\b/gi
300
+ /(^|\s)(Yarn|npm)(?!\S)/gi
289
301
  ],
290
302
  ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
291
303
  to: [
@@ -321,33 +333,20 @@ async function updateDXWorkflows() {
321
333
  processor: migrateWorkflow(sha)
322
334
  });
323
335
  }
324
- async function writePnpmWorkspaceFile(workspaces, packageExtensions) {
325
- const pnpmWorkspace = {
326
- packageExtensions,
327
- packages: workspaces.length > 0 ? workspaces : ["apps/*", "packages/*"]
328
- };
329
- const yamlContent = YAML4.stringify(pnpmWorkspace);
330
- await fs.writeFile("pnpm-workspace.yaml", yamlContent, "utf-8");
331
- }
332
- var apply = async (info) => {
336
+ var usePnpm = async (packageManager, currentNodeVersion) => {
333
337
  const minNodeVersion = "20.19.5";
334
- const currentNodeVersion = process.versions.node;
335
- if (!semver.gte(currentNodeVersion, minNodeVersion)) {
336
- console.error(
337
- `This codemod requires Node.js >= ${minNodeVersion}. Current version: ${currentNodeVersion}`
338
- );
339
- process.exit(1);
340
- }
341
- if (info.packageManager === "pnpm") {
342
- throw new Error("Project is already using pnpm");
343
- }
344
- const pm = info.packageManager === "yarn" ? new Yarn() : new NPM();
338
+ assert.notEqual(packageManager, "pnpm", "Project is already using pnpm");
339
+ assert.ok(
340
+ semver.gte(currentNodeVersion, minNodeVersion),
341
+ `his codemod requires Node.js >= ${minNodeVersion}. Current version: ${currentNodeVersion}`
342
+ );
345
343
  const logger = getLogger4(["dx-cli", "codemod"]);
344
+ const pm = packageManager === "yarn" ? new Yarn() : new NPM();
346
345
  const localWorkspaces = await pm.listWorkspaces();
347
- logger.info("Using the {protocol} protocol for local dependencies", {
348
- protocol: "workspace:"
349
- });
350
346
  if (localWorkspaces.length > 0) {
347
+ logger.info("Using the {protocol} protocol for local dependencies", {
348
+ protocol: "workspace:"
349
+ });
351
350
  await replaceInFile3({
352
351
  allowEmptyPaths: true,
353
352
  files: ["**/package.json"],
@@ -359,12 +358,11 @@ var apply = async (info) => {
359
358
  file: "package.json"
360
359
  });
361
360
  const workspaces = await preparePackageJsonForPnpm();
362
- const packageExtensions = info.packageManager === "yarn" ? await extractPackageExtensions() : void 0;
361
+ const packageExtensions = packageManager === "yarn" ? await extractPackageExtensions() : void 0;
363
362
  logger.info("Create {file}", {
364
363
  file: "pnpm-workspace.yaml"
365
364
  });
366
365
  await writePnpmWorkspaceFile(workspaces, packageExtensions);
367
- await $`corepack pnpm@latest add --config pnpm-plugin-pagopa`;
368
366
  logger.info("Remove node_modules and yarn files");
369
367
  await removeFiles(
370
368
  ".yarnrc",
@@ -375,8 +373,8 @@ var apply = async (info) => {
375
373
  ".pnp.loader.cjs",
376
374
  "node_modules"
377
375
  );
378
- const stat2 = await fs.stat(pm.lockFileName);
379
- if (stat2.isFile()) {
376
+ const stat = await fs.stat(pm.lockFileName);
377
+ if (stat.isFile()) {
380
378
  logger.info("Importing {source} to {target}", {
381
379
  source: pm.lockFileName,
382
380
  target: "pnpm-lock.yaml"
@@ -395,6 +393,16 @@ var apply = async (info) => {
395
393
  logger.info("Setting pnpm as the package manager...");
396
394
  await $`corepack use pnpm@latest`;
397
395
  };
396
+ var apply = async (info) => {
397
+ const logger = getLogger4(["dx-cli", "codemod"]);
398
+ try {
399
+ await usePnpm(info.packageManager, process.versions.node);
400
+ } catch (error) {
401
+ if (error instanceof Error) {
402
+ logger.error(error.message);
403
+ }
404
+ }
405
+ };
398
406
  var use_pnpm_default = {
399
407
  apply,
400
408
  description: "Migrate the project to use pnpm as the package manager",
@@ -757,10 +765,38 @@ import loadMonorepoScaffolder, {
757
765
  import chalk from "chalk";
758
766
  import { Command as Command4 } from "commander";
759
767
  import { $ as $3 } from "execa";
760
- import { okAsync as okAsync2, ResultAsync as ResultAsync6 } from "neverthrow";
768
+ import { errAsync, okAsync as okAsync2, ResultAsync as ResultAsync6 } from "neverthrow";
761
769
  import * as path from "path";
762
770
  import { oraPromise } from "ora";
763
771
 
772
+ // src/domain/github.ts
773
+ var PullRequest = class {
774
+ constructor(url) {
775
+ this.url = url;
776
+ }
777
+ };
778
+ var Repository = class {
779
+ constructor(name, owner) {
780
+ this.name = name;
781
+ this.owner = owner;
782
+ }
783
+ get fullName() {
784
+ return `${this.owner}/${this.name}`;
785
+ }
786
+ get ssh() {
787
+ return `git@github.com:${this.owner}/${this.name}.git`;
788
+ }
789
+ get url() {
790
+ return `https://github.com/${this.owner}/${this.name}`;
791
+ }
792
+ };
793
+ var RepositoryNotFoundError = class extends Error {
794
+ constructor(owner, name) {
795
+ super(`Repository ${owner}/${name} not found`);
796
+ this.name = "RepositoryNotFoundError";
797
+ }
798
+ };
799
+
764
800
  // src/adapters/execa/terraform.ts
765
801
  import { $ as $2 } from "execa";
766
802
  var tf$ = $2({
@@ -796,18 +832,6 @@ var decode = (schema) => (data) => ResultAsync5.fromPromise(
796
832
  );
797
833
 
798
834
  // src/adapters/commander/commands/init.ts
799
- var Repository = class {
800
- constructor(name, owner) {
801
- this.name = name;
802
- this.owner = owner;
803
- }
804
- get ssh() {
805
- return `git@github.com:${this.owner}/${this.name}.git`;
806
- }
807
- get url() {
808
- return `https://github.com/${this.owner}/${this.name}`;
809
- }
810
- };
811
835
  var withSpinner = (text, successText, failText, promise) => ResultAsync6.fromPromise(
812
836
  oraPromise(promise, {
813
837
  failText,
@@ -819,7 +843,20 @@ var withSpinner = (text, successText, failText, promise) => ResultAsync6.fromPro
819
843
  return new Error(failText, { cause });
820
844
  }
821
845
  );
822
- var validateAnswers = (answers) => okAsync2(answers);
846
+ var validateAnswers = (githubService) => (answers) => ResultAsync6.fromPromise(
847
+ githubService.getRepository(answers.repoOwner, answers.repoName),
848
+ (error) => error
849
+ ).andThen(
850
+ ({ fullName }) => errAsync(new Error(`Repository ${fullName} already exists.`))
851
+ ).orElse(
852
+ (error) => error instanceof RepositoryNotFoundError ? (
853
+ // If repository is not found, it's safe to proceed
854
+ okAsync2(answers)
855
+ ) : (
856
+ // Otherwise, propagate the error
857
+ errAsync(error)
858
+ )
859
+ ).map(() => answers);
823
860
  var runGeneratorActions = (generator) => (answers) => withSpinner(
824
861
  "Creating workspace files...",
825
862
  "Workspace files created successfully!",
@@ -911,8 +948,8 @@ var initializeGitRepository = (repository) => {
911
948
  pushToOrigin()
912
949
  ).map(() => ({ branchName, repository }));
913
950
  };
914
- var handleNewGitHubRepository = (octokit2) => (answers) => createRemoteRepository(answers).andThen(initializeGitRepository).andThen(
915
- (localWorkspace) => createPullRequest(octokit2)(localWorkspace).map((pr) => ({
951
+ var handleNewGitHubRepository = (githubService) => (answers) => createRemoteRepository(answers).andThen(initializeGitRepository).andThen(
952
+ (localWorkspace) => createPullRequest(githubService)(localWorkspace).map((pr) => ({
916
953
  pr,
917
954
  repository: localWorkspace.repository
918
955
  }))
@@ -928,14 +965,14 @@ var makeInitResult = (answers, { pr, repository }) => {
928
965
  repository
929
966
  };
930
967
  };
931
- var createPullRequest = (octokit2) => ({
968
+ var createPullRequest = (githubService) => ({
932
969
  branchName,
933
970
  repository
934
971
  }) => withSpinner(
935
972
  "Creating Pull Request...",
936
973
  "Pull Request created successfully!",
937
974
  "Failed to create Pull Request.",
938
- octokit2.rest.pulls.create({
975
+ githubService.createPullRequest({
939
976
  base: "main",
940
977
  body: "This PR contains the scaffolded monorepo structure.",
941
978
  head: branchName,
@@ -943,9 +980,9 @@ var createPullRequest = (octokit2) => ({
943
980
  repo: repository.name,
944
981
  title: "Scaffold repository"
945
982
  })
946
- ).map(({ data }) => ({ url: data.html_url })).orElse(() => okAsync2(void 0));
983
+ ).orElse(() => okAsync2(void 0));
947
984
  var makeInitCommand = ({
948
- octokit: octokit2
985
+ gitHubService: gitHubService2
949
986
  }) => new Command4().name("init").description(
950
987
  "Command to initialize resources (like projects, subscriptions, ...)"
951
988
  ).addCommand(
@@ -953,10 +990,10 @@ var makeInitCommand = ({
953
990
  await checkPreconditions().andThen(initPlop).andTee(loadMonorepoScaffolder).andThen((plop) => getGenerator(plop)(PLOP_MONOREPO_GENERATOR_NAME)).andThen(
954
991
  (generator) => (
955
992
  // Ask the user the questions defined in the plop generator
956
- getPrompts(generator).andThen(decode(answersSchema)).andThen(validateAnswers).andThen(runGeneratorActions(generator))
993
+ getPrompts(generator).andThen(decode(answersSchema)).andThen(validateAnswers(gitHubService2)).andThen(runGeneratorActions(generator))
957
994
  )
958
995
  ).andThen(
959
- (answers) => handleNewGitHubRepository(octokit2)(answers).map(
996
+ (answers) => handleNewGitHubRepository(gitHubService2)(answers).map(
960
997
  (repoPr) => makeInitResult(answers, repoPr)
961
998
  )
962
999
  ).match(displaySummary, exitWithError(this));
@@ -996,7 +1033,7 @@ var makeSavemoneyCommand = () => new Command5("savemoney").description(
996
1033
  // src/adapters/commander/index.ts
997
1034
  var makeCli = (deps2, config2, cliDeps) => {
998
1035
  const program2 = new Command6();
999
- program2.name("dx").description("The CLI for DX-Platform").version("0.14.3");
1036
+ program2.name("dx").description("The CLI for DX-Platform").version("0.14.5");
1000
1037
  program2.addCommand(makeDoctorCommand(deps2, config2));
1001
1038
  program2.addCommand(makeCodemodCommand(cliDeps));
1002
1039
  program2.addCommand(makeInitCommand(deps2));
@@ -1039,11 +1076,11 @@ var parseJson = Result2.fromThrowable(
1039
1076
  );
1040
1077
 
1041
1078
  // src/adapters/node/fs/file-reader.ts
1042
- var readFile2 = (filePath) => ResultAsync7.fromPromise(
1079
+ var readFile = (filePath) => ResultAsync7.fromPromise(
1043
1080
  fs3.readFile(filePath, "utf-8"),
1044
1081
  (cause) => new Error(`Failed to read file: ${filePath}`, { cause })
1045
1082
  );
1046
- var readFileAndDecode = (filePath, schema) => readFile2(filePath).andThen(parseJson).andThen(decode(schema));
1083
+ var readFileAndDecode = (filePath, schema) => readFile(filePath).andThen(parseJson).andThen(decode(schema));
1047
1084
  var fileExists = (path3) => ResultAsync7.fromPromise(
1048
1085
  fs3.stat(path3),
1049
1086
  () => new Error(`${path3} not found.`)
@@ -1109,7 +1146,7 @@ var resolveWorkspacePattern = (repoRoot, pattern) => ResultAsync8.fromPromise(
1109
1146
  subDirectories.map((directory) => path2.join(repoRoot, directory))
1110
1147
  )
1111
1148
  );
1112
- var getWorkspaces = (repoRoot) => readFile2(path2.join(repoRoot, "pnpm-workspace.yaml")).andThen(parseYaml).andThen(
1149
+ var getWorkspaces = (repoRoot) => readFile(path2.join(repoRoot, "pnpm-workspace.yaml")).andThen(parseYaml).andThen(
1113
1150
  (obj) => (
1114
1151
  // If no packages are defined, go on with an empty array
1115
1152
  decode(z3.object({ packages: z3.array(z3.string()) }))(obj).orElse(
@@ -1141,9 +1178,54 @@ var makeRepositoryReader = () => ({
1141
1178
  fileExists,
1142
1179
  findRepositoryRoot,
1143
1180
  getWorkspaces,
1144
- readFile: readFile2
1181
+ readFile
1145
1182
  });
1146
1183
 
1184
+ // src/adapters/octokit/index.ts
1185
+ import { RequestError } from "octokit";
1186
+ var OctokitGitHubService = class {
1187
+ #octokit;
1188
+ constructor(octokit2) {
1189
+ this.#octokit = octokit2;
1190
+ }
1191
+ async createPullRequest(params) {
1192
+ try {
1193
+ const { data } = await this.#octokit.rest.pulls.create({
1194
+ base: params.base,
1195
+ body: params.body,
1196
+ head: params.head,
1197
+ owner: params.owner,
1198
+ repo: params.repo,
1199
+ title: params.title
1200
+ });
1201
+ return new PullRequest(data.html_url);
1202
+ } catch (error) {
1203
+ throw new Error(
1204
+ `Failed to create pull request in ${params.owner}/${params.repo}`,
1205
+ {
1206
+ cause: error
1207
+ }
1208
+ );
1209
+ }
1210
+ }
1211
+ async getRepository(owner, name) {
1212
+ try {
1213
+ const { data } = await this.#octokit.rest.repos.get({
1214
+ owner,
1215
+ repo: name
1216
+ });
1217
+ return new Repository(data.name, data.owner.login);
1218
+ } catch (error) {
1219
+ if (error instanceof RequestError && error.status === 404) {
1220
+ throw new RepositoryNotFoundError(owner, name);
1221
+ }
1222
+ throw new Error(`Failed to fetch repository ${owner}/${name}`, {
1223
+ cause: error
1224
+ });
1225
+ }
1226
+ }
1227
+ };
1228
+
1147
1229
  // src/config.ts
1148
1230
  var getConfig = () => ({
1149
1231
  minVersions: {
@@ -1152,9 +1234,9 @@ var getConfig = () => ({
1152
1234
  });
1153
1235
 
1154
1236
  // src/use-cases/apply-codemod.ts
1155
- import { errAsync, okAsync as okAsync4, ResultAsync as ResultAsync9 } from "neverthrow";
1237
+ import { errAsync as errAsync2, okAsync as okAsync4, ResultAsync as ResultAsync9 } from "neverthrow";
1156
1238
  var getCodemodById = (registry2, id) => registry2.getById(id).andThen(
1157
- (codemod) => codemod ? okAsync4(codemod) : errAsync(new Error(`Codemod with id ${id} not found`))
1239
+ (codemod) => codemod ? okAsync4(codemod) : errAsync2(new Error(`Codemod with id ${id} not found`))
1158
1240
  );
1159
1241
  var safeGetInfo = (getInfo2) => ResultAsync9.fromPromise(
1160
1242
  getInfo2(),
@@ -1195,11 +1277,12 @@ await configure({
1195
1277
  var repositoryReader = makeRepositoryReader();
1196
1278
  var packageJsonReader = makePackageJsonReader();
1197
1279
  var validationReporter = makeValidationReporter();
1198
- var octokit = new Octokit2({
1280
+ var octokit = new Octokit3({
1199
1281
  auth: process.env.GITHUB_TOKEN
1200
1282
  });
1283
+ var gitHubService = new OctokitGitHubService(octokit);
1201
1284
  var deps = {
1202
- octokit,
1285
+ gitHubService,
1203
1286
  packageJsonReader,
1204
1287
  repositoryReader,
1205
1288
  validationReporter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/dx-cli",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "type": "module",
5
5
  "description": "A CLI useful to manage DX tools.",
6
6
  "repository": {