@cbs-consulting/generator-btp 1.2.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,20 @@ describe("generator-btp:cap", () => {
20
20
  customerName: "Test Customer",
21
21
  dbSchema: "TEST_SCHEMA",
22
22
  useAzureDevOps: true,
23
+ azureRepoUrl:
24
+ "https://dev.azure.com/myorg/myproject/_git/test-cap-project",
25
+ useBestPractices: true,
26
+ cdsFeatures: [
27
+ "typescript",
28
+ "mta",
29
+ "xsuaa",
30
+ "hana",
31
+ "sqlite",
32
+ "html5-repo",
33
+ "tiny-sample",
34
+ "http",
35
+ "test",
36
+ ],
23
37
  });
24
38
  }, 120000);
25
39
 
@@ -34,6 +48,18 @@ describe("generator-btp:cap", () => {
34
48
  });
35
49
  });
36
50
 
51
+ const defaultCdsFeatures = [
52
+ "typescript",
53
+ "mta",
54
+ "xsuaa",
55
+ "hana",
56
+ "sqlite",
57
+ "html5-repo",
58
+ "tiny-sample",
59
+ "http",
60
+ "test",
61
+ ];
62
+
37
63
  describe.each([
38
64
  {
39
65
  scenario: "with Azure DevOps enabled",
@@ -41,6 +67,10 @@ describe("generator-btp:cap", () => {
41
67
  customerName: "Test Customer",
42
68
  dbSchema: "TEST_CAP_PROJECT",
43
69
  useAzureDevOps: true,
70
+ azureRepoUrl:
71
+ "https://dev.azure.com/myorg/myproject/_git/test-cap-project",
72
+ useBestPractices: true,
73
+ cdsFeatures: defaultCdsFeatures,
44
74
  expectedProjectNormalized: "test-cap-project",
45
75
  expectedCustomerNormalized: "test-customer",
46
76
  expectedDbSchema: "TEST_CAP_PROJECT",
@@ -51,6 +81,7 @@ describe("generator-btp:cap", () => {
51
81
  customerName: "My Customer",
52
82
  dbSchema: null,
53
83
  useAzureDevOps: false,
84
+ cdsFeatures: defaultCdsFeatures,
54
85
  expectedProjectNormalized: "no-azure-project",
55
86
  expectedCustomerNormalized: "my-customer",
56
87
  expectedDbSchema: "NO_AZURE_PROJECT",
@@ -61,6 +92,7 @@ describe("generator-btp:cap", () => {
61
92
  customerName: "UPPERCASE CUSTOMER",
62
93
  dbSchema: null,
63
94
  useAzureDevOps: false,
95
+ cdsFeatures: defaultCdsFeatures,
64
96
  expectedProjectNormalized: "uppercase-project",
65
97
  expectedCustomerNormalized: "uppercase-customer",
66
98
  expectedDbSchema: "UPPERCASE_PROJECT",
@@ -71,10 +103,25 @@ describe("generator-btp:cap", () => {
71
103
  customerName: "Multi Space Customer",
72
104
  dbSchema: null,
73
105
  useAzureDevOps: true,
106
+ azureRepoUrl:
107
+ "https://dev.azure.com/myorg/myproject/_git/multi-space-project",
108
+ useBestPractices: true,
109
+ cdsFeatures: defaultCdsFeatures,
74
110
  expectedProjectNormalized: "multi-space-project",
75
111
  expectedCustomerNormalized: "multi-space-customer",
76
112
  expectedDbSchema: "MULTI_SPACE_PROJECT",
77
113
  },
114
+ {
115
+ scenario: "with custom CDS features",
116
+ projectName: "Custom Features Project",
117
+ customerName: "Custom Customer",
118
+ dbSchema: null,
119
+ useAzureDevOps: false,
120
+ cdsFeatures: ["typescript", "hana", "xsuaa"],
121
+ expectedProjectNormalized: "custom-features-project",
122
+ expectedCustomerNormalized: "custom-customer",
123
+ expectedDbSchema: "CUSTOM_FEATURES_PROJECT",
124
+ },
78
125
  ])(
79
126
  "$scenario",
80
127
  ({
@@ -82,6 +129,9 @@ describe("generator-btp:cap", () => {
82
129
  customerName,
83
130
  dbSchema,
84
131
  useAzureDevOps,
132
+ azureRepoUrl,
133
+ useBestPractices,
134
+ cdsFeatures,
85
135
  expectedProjectNormalized,
86
136
  expectedCustomerNormalized,
87
137
  expectedDbSchema,
@@ -89,14 +139,25 @@ describe("generator-btp:cap", () => {
89
139
  let runResult;
90
140
 
91
141
  beforeAll(async () => {
142
+ const prompts = {
143
+ projectName,
144
+ customerName,
145
+ dbSchema,
146
+ useAzureDevOps,
147
+ cdsFeatures,
148
+ };
149
+
150
+ // Add Azure DevOps-specific prompts if enabled
151
+ if (useAzureDevOps) {
152
+ Object.assign(prompts, {
153
+ azureRepoUrl,
154
+ useBestPractices,
155
+ });
156
+ }
157
+
92
158
  runResult = await helpers
93
159
  .run(join(__dirname, "../index.js"))
94
- .withPrompts({
95
- projectName,
96
- customerName,
97
- dbSchema,
98
- useAzureDevOps,
99
- })
160
+ .withPrompts(prompts)
100
161
  .withOptions({
101
162
  skipInstall: true,
102
163
  })
@@ -134,6 +195,7 @@ describe("generator-btp:cap", () => {
134
195
  expect(runResult.generator.answers.customerNameNormalized).toBe(
135
196
  expectedCustomerNormalized,
136
197
  );
198
+ expect(runResult.generator.answers.cdsFeatures).toEqual(cdsFeatures);
137
199
  });
138
200
 
139
201
  it("should create base configuration files", () => {
@@ -142,7 +204,6 @@ describe("generator-btp:cap", () => {
142
204
  ".npmrc",
143
205
  ".gitattributes",
144
206
  ".gitconfig.aliases",
145
- "base.tsconfig.json",
146
207
  "eslint.config.mjs",
147
208
  "prettier.config.mjs",
148
209
  "mta.yaml",
@@ -173,6 +234,22 @@ describe("generator-btp:cap", () => {
173
234
  }
174
235
  });
175
236
 
237
+ it("should conditionally create Azure DevOps setup scripts", () => {
238
+ if (useAzureDevOps) {
239
+ assert.file([
240
+ "scripts/setup-azure-devops/README.md",
241
+ "scripts/setup-azure-devops/azure-devops.env",
242
+ "scripts/setup-azure-devops/setup.sh",
243
+ ]);
244
+ } else {
245
+ assert.noFile([
246
+ "scripts/setup-azure-devops/README.md",
247
+ "scripts/setup-azure-devops/azure-devops.env",
248
+ "scripts/setup-azure-devops/setup.sh",
249
+ ]);
250
+ }
251
+ });
252
+
176
253
  it("should not create undeploy.json", () => {
177
254
  assert.noFile(["db/undeploy.json"]);
178
255
  });
@@ -212,4 +289,75 @@ describe("generator-btp:cap", () => {
212
289
  });
213
290
  },
214
291
  );
292
+
293
+ describe("with custom target directory", () => {
294
+ let runResult;
295
+
296
+ beforeAll(async () => {
297
+ runResult = await helpers
298
+ .run(join(__dirname, "../index.js"))
299
+ .withPrompts({
300
+ targetDirectory: "my-new-cap-project",
301
+ projectName: "My New CAP Project",
302
+ customerName: "Test Customer",
303
+ dbSchema: "MY_NEW_CAP_PROJECT",
304
+ useAzureDevOps: false,
305
+ cdsFeatures: [
306
+ "typescript",
307
+ "mta",
308
+ "xsuaa",
309
+ "hana",
310
+ "sqlite",
311
+ "html5-repo",
312
+ "tiny-sample",
313
+ "http",
314
+ "test",
315
+ ],
316
+ })
317
+ .withOptions({
318
+ skipInstall: true,
319
+ })
320
+ .withSpawnMock((command, args) => {
321
+ if (command === "git" && args[0] === "init") {
322
+ return;
323
+ }
324
+ if (command === "cds" && args[0] === "init") {
325
+ return;
326
+ }
327
+ if (command === "npm") {
328
+ return;
329
+ }
330
+ if (command === "npx" && args[0] === "simple-git-hooks") {
331
+ return;
332
+ }
333
+ });
334
+ }, 30000);
335
+
336
+ afterAll(() => {
337
+ if (runResult) {
338
+ runResult.restore();
339
+ }
340
+ });
341
+
342
+ it("should set the destination root to the target directory", () => {
343
+ expect(runResult.generator.destinationRoot()).toMatch(
344
+ /[\\/]my-new-cap-project$/,
345
+ );
346
+ });
347
+
348
+ it("should generate files inside the target directory", () => {
349
+ assert.file([
350
+ "my-new-cap-project/.gitignore",
351
+ "my-new-cap-project/mta.yaml",
352
+ "my-new-cap-project/.devcontainer/devcontainer.json",
353
+ ]);
354
+ });
355
+
356
+ it("should template package.json with normalized name inside target directory", () => {
357
+ assert.fileContent(
358
+ "my-new-cap-project/package.json",
359
+ /"name":\s*"my-new-cap-project"/,
360
+ );
361
+ });
362
+ });
215
363
  });
@@ -13,7 +13,7 @@
13
13
  "upgrade-deps": "ncu -u"
14
14
  },
15
15
  "simple-git-hooks": {
16
- "pre-commit": "npx lint-staged"
16
+ "pre-commit": "npx lint-staged && npm test"
17
17
  },
18
18
  "lint-staged": {
19
19
  "*.{ts,js,json,yml,yaml,xml}": "npm run format",
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noUnusedLocals": true,
4
+ "noUnusedParameters": true,
5
+ "allowUnusedLabels": false,
6
+ "allowUnreachableCode": false,
7
+ "noImplicitOverride": true,
8
+ "noImplicitReturns": true,
9
+ "noPropertyAccessFromIndexSignature": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "exactOptionalPropertyTypes": true
13
+ },
14
+ "exclude": [
15
+ "gen",
16
+ "**/*.config.mjs",
17
+ "**/*.config.js"
18
+ ]
19
+ }
@@ -10,8 +10,7 @@
10
10
  "typescript-eslint",
11
11
  "eslint-config-prettier",
12
12
  "@sap/eslint-plugin-cds@3",
13
- "@cap-js/cds-test",
14
- "ts-jest",
13
+ "@types/jest",
15
14
  "npm-check-updates"
16
15
  ]
17
16
  }
@@ -5,7 +5,12 @@ import fs from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import Generator from "yeoman-generator";
8
- import { mergeArray, readJsonC, readJsonCSafe } from "../../utils/jsonFile.js";
8
+ import {
9
+ mergeArray,
10
+ readJsonC,
11
+ readJsonCSafe,
12
+ sortPackageJson,
13
+ } from "../../utils/jsonFile.js";
9
14
 
10
15
  const __filename = fileURLToPath(import.meta.url);
11
16
  const __dirname = dirname(__filename);
@@ -19,6 +24,19 @@ export default class extends Generator {
19
24
  const aPrompt = [];
20
25
 
21
26
  aPrompt.push(
27
+ {
28
+ type: "input",
29
+ name: "targetDirectory",
30
+ message:
31
+ "Enter the target directory for the project (use '.' to generate in the current directory):",
32
+ default: ".",
33
+ validate(sInput) {
34
+ if (sInput.trim().length > 0) {
35
+ return true;
36
+ }
37
+ return "Target directory must not be empty.";
38
+ },
39
+ },
22
40
  {
23
41
  type: "input",
24
42
  name: "projectName",
@@ -57,9 +75,9 @@ export default class extends Generator {
57
75
  // Use project name in uppercase with underscores instead of special characters
58
76
  return answers.projectName
59
77
  .toUpperCase()
60
- .replace(/[^A-Z0-9]/g, '_')
61
- .replace(/_+/g, '_') // Replace multiple underscores with single underscore
62
- .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
78
+ .replace(/[^A-Z0-9]/g, "_")
79
+ .replace(/_+/g, "_") // Replace multiple underscores with single underscore
80
+ .replace(/^_|_$/g, ""); // Remove leading/trailing underscores
63
81
  },
64
82
  },
65
83
  {
@@ -68,10 +86,219 @@ export default class extends Generator {
68
86
  message: "Is Azure DevOps used for version control and CI/CD?",
69
87
  default: true,
70
88
  },
89
+ {
90
+ type: "checkbox",
91
+ name: "cdsFeatures",
92
+ message: "Select the CDS features to add (via cds add ...):",
93
+ choices: [
94
+ { name: "typescript", value: "typescript", checked: true },
95
+ { name: "mta", value: "mta", checked: true },
96
+ { name: "xsuaa", value: "xsuaa", checked: true },
97
+ { name: "hana", value: "hana", checked: true },
98
+ { name: "sqlite", value: "sqlite", checked: true },
99
+ { name: "html5-repo", value: "html5-repo", checked: true },
100
+ { name: "tiny-sample", value: "tiny-sample", checked: true },
101
+ { name: "http", value: "http", checked: true },
102
+ { name: "test", value: "test", checked: true },
103
+ ],
104
+ validate(selected) {
105
+ if (selected.length > 0) {
106
+ return true;
107
+ }
108
+ return "At least one CDS feature must be selected.";
109
+ },
110
+ },
71
111
  );
72
112
 
73
113
  const answers = await this.prompt(aPrompt);
74
114
 
115
+ // Normalize names for use in subsequent prompts and templates
116
+ answers.projectNameNormalized = answers.projectName
117
+ .toLowerCase()
118
+ .replace(/\s+/g, "-");
119
+ answers.customerNameNormalized = answers.customerName
120
+ .toLowerCase()
121
+ .replace(/\s+/g, "-");
122
+
123
+ // Change the destination root when a custom target directory is specified
124
+ const targetDir = answers.targetDirectory.trim();
125
+ if (targetDir !== ".") {
126
+ const resolvedTarget = resolve(this.destinationRoot(), targetDir);
127
+ fs.mkdirSync(resolvedTarget, { recursive: true });
128
+ this.destinationRoot(resolvedTarget);
129
+ }
130
+
131
+ // If using Azure DevOps, prompt for additional configuration
132
+ if (answers.useAzureDevOps) {
133
+ this.log(chalk.bold("\nāš™ļø Configuring Azure DevOps settings..."));
134
+
135
+ // First, ask for repository URL
136
+ const initialAzurePrompts = [
137
+ {
138
+ type: "input",
139
+ name: "azureRepoUrl",
140
+ message:
141
+ "Enter your Azure DevOps repository URL:\n (Example: https://dev.azure.com/ORG_NAME/PROJECT_NAME/_git/REPO_NAME)\n Your repository URL:",
142
+ validate(input) {
143
+ const regex =
144
+ /^https:\/\/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/]+)\/?$/;
145
+ if (regex.test(input.trim())) {
146
+ return true;
147
+ }
148
+ return "Invalid Azure DevOps repository URL. Expected format: https://dev.azure.com/ORG_NAME/PROJECT_NAME/_git/REPO_NAME";
149
+ },
150
+ },
151
+ {
152
+ type: "confirm",
153
+ name: "useBestPractices",
154
+ message:
155
+ "Use recommended best practices for branch policies and merge strategies?\n Choose no to customize settings in detail with best practices as defaults.",
156
+ default: true,
157
+ },
158
+ ];
159
+
160
+ const initialAzureAnswers = await this.prompt(initialAzurePrompts);
161
+ Object.assign(answers, initialAzureAnswers);
162
+
163
+ // Parse Azure DevOps repository URL to extract organization, project, and repo names
164
+ const urlRegex =
165
+ /^https:\/\/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/]+)\/?$/;
166
+ const match = answers.azureRepoUrl.trim().match(urlRegex);
167
+
168
+ if (match) {
169
+ answers.azureDevOpsOrgUrl = `https://dev.azure.com/${match[1]}`;
170
+ answers.azureProjectName = match[2];
171
+ answers.repoName = match[3];
172
+ }
173
+
174
+ // If not using best practices, ask for detailed configuration
175
+ if (!answers.useBestPractices) {
176
+ const detailedAzurePrompts = [
177
+ {
178
+ type: "input",
179
+ name: "mainBranch",
180
+ message: "Enter the name of the main/production branch:",
181
+ default: "main",
182
+ },
183
+ {
184
+ type: "input",
185
+ name: "devBranch",
186
+ message: "Enter the name of the development branch:",
187
+ default: "development",
188
+ },
189
+ {
190
+ type: "confirm",
191
+ name: "setDevBranchAsDefault",
192
+ message: "Set development branch as the default branch?",
193
+ default: true,
194
+ },
195
+ {
196
+ type: "number",
197
+ name: "minReviewers",
198
+ message: "Minimum number of reviewers required for PRs to main:",
199
+ default: 1,
200
+ validate(input) {
201
+ return input >= 0 ? true : "Must be 0 or greater";
202
+ },
203
+ },
204
+ {
205
+ type: "confirm",
206
+ name: "mainResetOnSourcePush",
207
+ message:
208
+ "Reset approvals when new commits are pushed to PRs (main branch)?",
209
+ default: true,
210
+ },
211
+ {
212
+ type: "confirm",
213
+ name: "mainAllowSquashMerge",
214
+ message: "Allow squash merge on main branch?",
215
+ default: true,
216
+ },
217
+ {
218
+ type: "confirm",
219
+ name: "mainAllowRebaseMerge",
220
+ message: "Allow rebase with merge commit on main branch?",
221
+ default: true,
222
+ },
223
+ {
224
+ type: "confirm",
225
+ name: "mainAllowRebase",
226
+ message: "Allow rebase and fast-forward on main branch?",
227
+ default: false,
228
+ },
229
+ {
230
+ type: "confirm",
231
+ name: "mainAllowNoFastForward",
232
+ message: "Allow basic merge (no fast-forward) on main branch?",
233
+ default: false,
234
+ },
235
+ {
236
+ type: "confirm",
237
+ name: "devAllowSquashMerge",
238
+ message: "Allow squash merge on development branch?",
239
+ default: true,
240
+ },
241
+ {
242
+ type: "confirm",
243
+ name: "devAllowRebaseMerge",
244
+ message: "Allow rebase with merge commit on development branch?",
245
+ default: true,
246
+ },
247
+ {
248
+ type: "confirm",
249
+ name: "devAllowRebase",
250
+ message: "Allow rebase and fast-forward on development branch?",
251
+ default: false,
252
+ },
253
+ {
254
+ type: "confirm",
255
+ name: "devAllowNoFastForward",
256
+ message:
257
+ "Allow basic merge (no fast-forward) on development branch?",
258
+ default: false,
259
+ },
260
+ {
261
+ type: "input",
262
+ name: "pipelineName",
263
+ message: "Enter the Azure Pipeline name:",
264
+ default: answers.projectNameNormalized,
265
+ },
266
+ {
267
+ type: "input",
268
+ name: "pipelineYamlPath",
269
+ message: "Enter the path to the pipeline YAML file:",
270
+ default: "azure-pipelines.yml",
271
+ },
272
+ ];
273
+
274
+ const detailedAzureAnswers = await this.prompt(detailedAzurePrompts);
275
+ Object.assign(answers, detailedAzureAnswers);
276
+ } else {
277
+ // Use best practice defaults
278
+ answers.mainBranch = "main";
279
+ answers.devBranch = "development";
280
+ answers.setDevBranchAsDefault = true;
281
+ answers.minReviewers = 1;
282
+ answers.mainResetOnSourcePush = true;
283
+ answers.mainAllowSquashMerge = true;
284
+ answers.mainAllowRebaseMerge = true;
285
+ answers.mainAllowRebase = false;
286
+ answers.mainAllowNoFastForward = false;
287
+ answers.devAllowSquashMerge = true;
288
+ answers.devAllowRebaseMerge = true;
289
+ answers.devAllowRebase = false;
290
+ answers.devAllowNoFastForward = false;
291
+ answers.pipelineName = answers.projectNameNormalized;
292
+ answers.pipelineYamlPath = "azure-pipelines.yml";
293
+
294
+ this.log(
295
+ chalk.green(
296
+ "āœ“ Using best practice defaults for Azure DevOps configuration",
297
+ ),
298
+ );
299
+ }
300
+ }
301
+
75
302
  // Check if .git directory exists, if not run git init
76
303
  if (!fs.existsSync(this.destinationPath(".git"))) {
77
304
  this.log(chalk.bold("\nšŸ”§ Initializing git repository..."));
@@ -82,22 +309,12 @@ export default class extends Generator {
82
309
  this.log(chalk.bold("\nšŸš€ Initializing CAP project..."));
83
310
  this.spawnCommandSync(
84
311
  "cds",
85
- [
86
- "init",
87
- "--add",
88
- "typescript,mta,xsuaa,hana,sqlite,html5-repo,tiny-sample,http",
89
- ],
312
+ ["init", "--add", answers.cdsFeatures.join(",")],
90
313
  {
91
314
  cwd: this.destinationPath(),
92
315
  },
93
316
  );
94
317
 
95
- answers.projectNameNormalized = answers.projectName
96
- .toLowerCase()
97
- .replace(/\s+/g, "-");
98
- answers.customerNameNormalized = answers.customerName
99
- .toLowerCase()
100
- .replace(/\s+/g, "-");
101
318
  this.answers = answers;
102
319
  }
103
320
 
@@ -148,7 +365,25 @@ export default class extends Generator {
148
365
  this.fs.copyTpl(sOrigin, sTarget, this.answers);
149
366
  });
150
367
 
151
- // 3) Merge additions into existing files (scripting.txt is excluded by design)
368
+ // 3) Copy Azure DevOps setup files from the setup-azure-devops generator
369
+ if (this.answers.useAzureDevOps) {
370
+ const azureDevOpsTemplatesPath = resolve(
371
+ __dirname,
372
+ "../setup-azure-devops/templates",
373
+ );
374
+ this.sourceRoot(azureDevOpsTemplatesPath);
375
+ globSync("**", {
376
+ cwd: this.sourceRoot(),
377
+ nodir: true,
378
+ }).forEach((fileName) => {
379
+ const sOrigin = join(azureDevOpsTemplatesPath, fileName);
380
+ const sTarget = this.destinationPath(fileName);
381
+
382
+ this.fs.copyTpl(sOrigin, sTarget, this.answers);
383
+ });
384
+ }
385
+
386
+ // 4) Merge additions into existing files (scripting.txt is excluded by design)
152
387
  // package.json
153
388
  const destPkgPath = this.destinationPath("package.json");
154
389
  const srcPkgPath = join(this._addRoot, "package.json");
@@ -156,15 +391,15 @@ export default class extends Generator {
156
391
  const addPkg = readJsonC(srcPkgPath, {});
157
392
  const mergedPkg = mergewith({}, destPkg, addPkg, mergeArray);
158
393
  mergedPkg.name = this.answers.projectNameNormalized;
159
- this.fs.writeJSON(destPkgPath, mergedPkg);
394
+ this.fs.writeJSON(destPkgPath, sortPackageJson(mergedPkg));
160
395
 
161
- // tsconfig.json -> ensure it extends base.tsconfig.json
396
+ // tsconfig.json
162
397
  const destTsPath = this.destinationPath("tsconfig.json");
398
+ const srcTsPath = join(this._addRoot, "tsconfig.json");
163
399
  const destTs = readJsonCSafe(this, destTsPath, {});
164
- if (destTs.extends !== "./base.tsconfig.json") {
165
- destTs.extends = "./base.tsconfig.json";
166
- }
167
- this.fs.writeJSON(destTsPath, destTs);
400
+ const addTs = readJsonC(srcTsPath, {});
401
+ const mergedTs = mergewith({}, destTs, addTs, mergeArray);
402
+ this.fs.writeJSON(destTsPath, mergedTs);
168
403
 
169
404
  // .vscode/settings.json
170
405
  const destVsSettingsPath = this.destinationPath(".vscode/settings.json");
@@ -220,5 +455,21 @@ export default class extends Generator {
220
455
  this.log(chalk.bold.green("\nāœ… CAP best practices applied."));
221
456
  this.log("Next steps:");
222
457
  this.log("1) Review/commit the changes.");
458
+
459
+ if (this.answers.useAzureDevOps) {
460
+ this.log("2) Configure Azure DevOps branch policies and pipeline:");
461
+ this.log(
462
+ " a) Review/edit scripts/setup-azure-devops/azure-devops.env if needed",
463
+ );
464
+ this.log(
465
+ " b) Ensure you're logged in to Azure CLI: az login --allow-no-subscriptions",
466
+ );
467
+ this.log(
468
+ " c) Run the setup script: bash scripts/setup-azure-devops/setup.sh",
469
+ );
470
+ this.log(
471
+ " d) See scripts/setup-azure-devops/README.md for detailed instructions",
472
+ );
473
+ }
223
474
  }
224
475
  }
@@ -1,5 +1,6 @@
1
1
  [alias]
2
2
  # Helper aliases for ticket workflow
3
+ cleanup-branches = "!f() {\n current_branch=$(git symbolic-ref --short HEAD);\n default_branch=$(git default-branch);\n if [ $? -ne 0 ]; then\n echo \"Error: Could not determine default branch\" >&2;\n return 1;\n fi;\n deleted_any=false;\n\n for branch in $(git branch --format=\"%(refname:short)\"); do\n if [ \"$branch\" = \"$current_branch\" ] || [ \"$branch\" = \"$default_branch\" ]; then\n continue;\n fi\n\n upstream=$(git for-each-ref --format=\"%(upstream:short)\" refs/heads/\"$branch\")\n if [ -z \"$upstream\" ]; then\n echo \" Skipping $branch - No upstream tracking branch\"\n continue;\n fi\n\n if git branch -d \"$branch\" >/dev/null 2>&1; then\n if [ \"$deleted_any\" = false ]; then\n echo \"Deleting branches:\";\n deleted_any=true;\n fi\n echo \" $branch - Remote: $upstream\"\n else\n echo \" Not deleting $branch - Remote: $upstream (not merged or has unpushed commits)\"\n fi\n done\n\n if [ \"$deleted_any\" = false ]; then\n echo \"No branches to delete.\"\n fi\n}; f"
3
4
  extract-branch-parts = "!f() { current_branch=$(git branch --show-current); case \"$current_branch\" in */*) prefix=\"${current_branch%%/*}\"; ticketRef=\"${current_branch#*/}\"; echo \"$prefix|$ticketRef\" ;; *) echo \"Error: Branch name must contain a prefix (e.g., feature/ABC-123)\" >&2; return 1 ;; esac; }; f"
4
5
  capitalize-prefix = "!f() { first_char=$(printf '%s' \"$1\" | cut -c1 | tr '[:lower:]' '[:upper:]'); rest_chars=$(printf '%s' \"$1\" | cut -c2-); echo \"$first_char$rest_chars\"; }; f"
5
6
  format-pr-url = "!f() { pr_id=\"$1\"; remote_url=$(git config --get remote.origin.url | sed 's|https://[^@]*@|https://|'); base_url=$(echo \"$remote_url\" | sed 's|\\.git$||'); echo \"${base_url}/pullrequest/${pr_id}\"; }; f"
@@ -8,6 +8,7 @@ default-*.json
8
8
  gen/
9
9
  node_modules/
10
10
  target/
11
+ coverage/
11
12
 
12
13
  # Web IDE, App Studio
13
14
  .che/
@@ -0,0 +1,25 @@
1
+ {
2
+ "clearMocks": true,
3
+ "coverageDirectory": "coverage",
4
+ "collectCoverageFrom": ["./srv/**", "!./srv/types/**"],
5
+ "collectCoverage": true,
6
+ "coverageProvider": "v8",
7
+ "coverageThreshold": {
8
+ "global": {
9
+ "branches": 50,
10
+ "functions": 50,
11
+ "lines": 70,
12
+ "statements": 70
13
+ }
14
+ },
15
+ "roots": ["<rootDir>"],
16
+ "preset": "ts-jest",
17
+ "globals": {},
18
+ "testEnvironment": "node",
19
+ "testMatch": ["**/?(*.)+(spec|test).[tj]s?(x)"],
20
+ "testPathIgnorePatterns": ["<rootDir>/dist/", "<rootDir>/__archive/", "<rootDir>/node_modules/", "<rootDir>/config/", "<rootDir>/gen/"],
21
+ "modulePathIgnorePatterns": ["<rootDir>/gen"],
22
+ "transform": {
23
+ "^.+\\.(ts|tsx)$": ["ts-jest"]
24
+ }
25
+ }