@sap-ux/create 1.0.60 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -288,15 +288,18 @@ Example:
288
288
 
289
289
  `npx --yes @sap-ux/create@latest add system --name "My System" --url https://my-sap.example.com --client 100 --username myuser`
290
290
 
291
+ `npx --yes @sap-ux/create@latest add system` (interactive mode)
292
+
291
293
  Options:
292
- - `--name <string>` _(required)_ - Display name for the system
293
- - `--url <string>` _(required)_ - URL of the backend system
294
+ - `--name <string>` - Display name for the system
295
+ - `--url <string>` - URL of the backend system
294
296
  - `--client <string>` - SAP client number (optional)
295
297
  - `--type <string>` - System type (AbapCloud | OnPrem | Generic) _(default: `OnPrem`)_
296
298
  - `--auth <string>` - Authentication type (basic | reentranceTicket | oauth2 | oauth2ClientCredential) _(default: `basic`)_
297
299
  - `--connection-type <string>` - Connection type (abap_catalog | generic_host | odata_service) _(default: `abap_catalog`)_
298
300
  - `--username <string>` - Username for basic authentication
299
301
  - `--password <string>` - To avoid plain-text credentials in the shell's history, pass an env reference: --password env:MY_VAR
302
+ - `--skip-check` - Skip connection verification before saving
300
303
 
301
304
  --------------------------------
302
305
 
@@ -398,9 +401,12 @@ Example:
398
401
 
399
402
  `npx --yes @sap-ux/create@latest remove system --url https://my-sap.example.com --client 100`
400
403
 
404
+ `npx --yes @sap-ux/create@latest remove system` (interactive mode)
405
+
401
406
  Options:
402
- - `--url <string>` _(required)_ - URL of the backend system to remove
407
+ - `--url <string>` - URL of the backend system to remove
403
408
  - `--client <string>` - SAP client number (optional)
409
+ - `--force` - Skip confirmation prompt
404
410
 
405
411
  --------------------------------
406
412
 
@@ -426,13 +432,16 @@ Example:
426
432
 
427
433
  `npx --yes @sap-ux/create@latest update system --url https://my-sap.example.com --client 100 --username newuser`
428
434
 
435
+ `npx --yes @sap-ux/create@latest update system` (interactive mode)
436
+
429
437
  Options:
430
- - `--url <string>` _(required)_ - URL of the backend system to update
438
+ - `--url <string>` - URL of the backend system to update
431
439
  - `--client <string>` - SAP client number to identify the system (optional)
432
440
  - `--name <string>` - New display name for the system
433
441
  - `--username <string>` - New username
434
442
  - `--password <string>` - To avoid plain-text credentials in the shell's history, pass an env reference: --password env:MY_VAR
435
443
  - `--clear-credentials` - Remove stored credentials from the system
444
+ - `--skip-check` - Skip connection verification before saving
436
445
 
437
446
  --------------------------------
438
447
 
@@ -1,8 +1,11 @@
1
1
  import { isAppStudio } from '@sap-ux/btp-utils';
2
- import { getService, BackendSystem, BackendSystemKey, SystemType, AuthenticationType, ConnectionType } from '@sap-ux/store';
2
+ import { getService, BackendSystem, BackendSystemKey, SystemType, AuthenticationType, ConnectionType, isSystemNameInUse } from '@sap-ux/store';
3
3
  import { replaceEnvVariables } from '@sap-ux/ui5-config';
4
4
  import { config as loadEnvConfig } from 'dotenv';
5
+ import { validateClient } from '@sap-ux/project-input-validator';
5
6
  import { getLogger } from '../../tracing/index.js';
7
+ import { promptForSystemConfig } from '../utils/system-prompts.js';
8
+ import { checkConnectionOrPrompt } from '../utils/system-connection.js';
6
9
  /**
7
10
  * Add the "add system" subcommand to a passed command.
8
11
  * Adds a new backend system to the saved systems store (~/.fioritools).
@@ -18,15 +21,17 @@ export function addSystemAddCommand(cmd) {
18
21
  Connection types: \`${Object.values(ConnectionType).join('`, `')}\`\n
19
22
  Example:
20
23
  \`npx --yes @sap-ux/create@latest add system --name "My System" --url https://my-sap.example.com\`
21
- \`npx --yes @sap-ux/create@latest add system --name "My System" --url https://my-sap.example.com --client 100 --username myuser\``)
22
- .requiredOption('--name <string>', 'Display name for the system')
23
- .requiredOption('--url <string>', 'URL of the backend system')
24
+ \`npx --yes @sap-ux/create@latest add system --name "My System" --url https://my-sap.example.com --client 100 --username myuser\`
25
+ \`npx --yes @sap-ux/create@latest add system\` (interactive mode)`)
26
+ .option('--name <string>', 'Display name for the system')
27
+ .option('--url <string>', 'URL of the backend system')
24
28
  .option('--client <string>', 'SAP client number (optional)')
25
29
  .option('--type <string>', `System type (${Object.values(SystemType).join(' | ')})`, SystemType.AbapOnPrem)
26
30
  .option('--auth <string>', `Authentication type (${Object.values(AuthenticationType).join(' | ')})`, AuthenticationType.Basic)
27
31
  .option('--connection-type <string>', `Connection type (${Object.values(ConnectionType).join(' | ')})`, ConnectionType.AbapCatalog)
28
32
  .option('--username <string>', 'Username for basic authentication')
29
33
  .option('--password <string>', "To avoid plain-text credentials in the shell's history, pass an env reference: --password env:MY_VAR")
34
+ .option('--skip-check', 'Skip connection verification before saving')
30
35
  .action(async (options) => {
31
36
  loadEnvConfig();
32
37
  await addSystem({
@@ -37,10 +42,89 @@ Example:
37
42
  authenticationType: options.auth,
38
43
  connectionType: options.connectionType,
39
44
  username: options.username,
40
- password: options.password
45
+ password: options.password,
46
+ skipCheck: !!options.skipCheck
41
47
  });
42
48
  });
43
49
  }
50
+ /**
51
+ * Validates the system configuration fields.
52
+ *
53
+ * @param config - system configuration to validate
54
+ * @param config.name - display name
55
+ * @param config.url - URL of the backend system
56
+ * @param config.client - optional SAP client
57
+ * @param config.systemType - system type
58
+ * @param config.authenticationType - authentication type
59
+ * @param config.connectionType - connection type
60
+ * @param logger - logger instance
61
+ * @returns true if valid, false otherwise
62
+ */
63
+ function validateSystemConfig(config, logger) {
64
+ if (!config.name || !config.url || !config.systemType || !config.authenticationType || !config.connectionType) {
65
+ logger.error('Missing required fields. System was not added.');
66
+ return false;
67
+ }
68
+ if (config.name.trim().length === 0) {
69
+ logger.error('System name cannot be empty or whitespace-only.');
70
+ return false;
71
+ }
72
+ try {
73
+ new URL(config.url);
74
+ }
75
+ catch {
76
+ logger.error(`Invalid URL: '${config.url}'`);
77
+ return false;
78
+ }
79
+ if (config.client !== undefined && config.client !== '') {
80
+ const clientValidation = validateClient(config.client);
81
+ if (clientValidation !== true) {
82
+ logger.error(`Invalid client '${config.client}'. Leave this field empty or enter a value between 000-999.`);
83
+ return false;
84
+ }
85
+ }
86
+ const validSystemTypes = Object.values(SystemType);
87
+ if (!validSystemTypes.includes(config.systemType)) {
88
+ logger.error(`Invalid system type '${config.systemType}'. Valid values: ${validSystemTypes.join(', ')}`);
89
+ return false;
90
+ }
91
+ const validAuthTypes = Object.values(AuthenticationType);
92
+ if (!validAuthTypes.includes(config.authenticationType)) {
93
+ logger.error(`Invalid auth type '${config.authenticationType}'. Valid values: ${validAuthTypes.join(', ')}`);
94
+ return false;
95
+ }
96
+ const validConnectionTypes = Object.values(ConnectionType);
97
+ if (!validConnectionTypes.includes(config.connectionType)) {
98
+ logger.error(`Invalid connection type '${config.connectionType}'. Valid values: ${validConnectionTypes.join(', ')}`);
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ /**
104
+ * Checks for duplicate systems in the store.
105
+ *
106
+ * @param config - system configuration
107
+ * @param config.name - system display name
108
+ * @param config.url - system URL
109
+ * @param config.client - optional SAP client
110
+ * @param service - backend system service
111
+ * @param logger - logger instance
112
+ * @returns true if no duplicates found, false otherwise
113
+ */
114
+ async function checkForDuplicates(config, service, logger) {
115
+ const existingSystem = await service.read(new BackendSystemKey({ url: config.url, client: config.client }));
116
+ if (existingSystem) {
117
+ const clientSuffix = config.client ? ` (client ${config.client})` : '';
118
+ logger.error(`System '${config.url}'${clientSuffix} already exists. Use 'update system' to update it.`);
119
+ return false;
120
+ }
121
+ const nameExists = await isSystemNameInUse(config.name);
122
+ if (nameExists) {
123
+ logger.error(`A system with the name '${config.name}' already exists. Please choose a different name.`);
124
+ return false;
125
+ }
126
+ return true;
127
+ }
44
128
  /**
45
129
  * Adds a backend system to the saved systems store.
46
130
  *
@@ -53,6 +137,7 @@ Example:
53
137
  * @param params.connectionType - connection type
54
138
  * @param params.username - optional username for basic auth
55
139
  * @param params.password - optional password for basic auth
140
+ * @param params.skipCheck - skip connection verification
56
141
  */
57
142
  async function addSystem(params) {
58
143
  const logger = getLogger();
@@ -61,37 +146,7 @@ async function addSystem(params) {
61
146
  logger.error('System management using the CLI is not supported in SAP Business Application Studio. Use the built-in system management instead.');
62
147
  return;
63
148
  }
64
- try {
65
- new URL(params.url);
66
- }
67
- catch {
68
- logger.error(`Invalid URL: '${params.url}'`);
69
- return;
70
- }
71
- const validSystemTypes = Object.values(SystemType);
72
- if (!validSystemTypes.includes(params.systemType)) {
73
- logger.error(`Invalid system type '${params.systemType}'. Valid values: ${validSystemTypes.join(', ')}`);
74
- return;
75
- }
76
- const validAuthTypes = Object.values(AuthenticationType);
77
- if (!validAuthTypes.includes(params.authenticationType)) {
78
- logger.error(`Invalid auth type '${params.authenticationType}'. Valid values: ${validAuthTypes.join(', ')}`);
79
- return;
80
- }
81
- const validConnectionTypes = Object.values(ConnectionType);
82
- if (!validConnectionTypes.includes(params.connectionType)) {
83
- logger.error(`Invalid connection type '${params.connectionType}'. Valid values: ${validConnectionTypes.join(', ')}`);
84
- return;
85
- }
86
- const service = await getService({ entityName: 'system' });
87
- const existingSystem = await service.read(new BackendSystemKey({ url: params.url, client: params.client }));
88
- if (existingSystem) {
89
- const clientSuffix = params.client ? ` (client ${params.client})` : '';
90
- logger.error(`System '${params.url}'${clientSuffix} already exists. Use 'update system' to update it.`);
91
- return;
92
- }
93
- replaceEnvVariables(params);
94
- const system = new BackendSystem({
149
+ const config = await promptForSystemConfig({
95
150
  name: params.name,
96
151
  url: params.url,
97
152
  client: params.client,
@@ -101,8 +156,38 @@ async function addSystem(params) {
101
156
  username: params.username,
102
157
  password: params.password
103
158
  });
159
+ replaceEnvVariables(config);
160
+ if (!validateSystemConfig(config, logger)) {
161
+ return;
162
+ }
163
+ const service = await getService({ entityName: 'system' });
164
+ if (!(await checkForDuplicates(config, service, logger))) {
165
+ return;
166
+ }
167
+ const shouldSave = await checkConnectionOrPrompt({
168
+ url: config.url,
169
+ client: config.client,
170
+ systemType: config.systemType,
171
+ authenticationType: config.authenticationType,
172
+ username: config.username,
173
+ password: config.password
174
+ }, params.skipCheck || false);
175
+ if (!shouldSave) {
176
+ logger.info('System was not saved.');
177
+ return;
178
+ }
179
+ const system = new BackendSystem({
180
+ name: config.name,
181
+ url: config.url,
182
+ client: config.client,
183
+ systemType: config.systemType,
184
+ authenticationType: config.authenticationType,
185
+ connectionType: config.connectionType,
186
+ username: config.username,
187
+ password: config.password
188
+ });
104
189
  await service.write(system);
105
- logger.info(`System '${params.name}' added.`);
190
+ logger.info(`System '${config.name}' added.`);
106
191
  }
107
192
  catch (error) {
108
193
  logger.error(error.message);
@@ -1,6 +1,7 @@
1
1
  import { isAppStudio } from '@sap-ux/btp-utils';
2
2
  import { getService, BackendSystemKey } from '@sap-ux/store';
3
3
  import { getLogger } from '../../tracing/index.js';
4
+ import { promptForSystemIdentifier, promptForRemoveConfirmation } from '../utils/system-prompts.js';
4
5
  /**
5
6
  * Add the "remove system" subcommand to a passed command.
6
7
  * Removes a saved backend system from the store (~/.fioritools) and deletes its credentials from the OS keychain.
@@ -12,11 +13,13 @@ export function addSystemRemoveCommand(cmd) {
12
13
  .description(`Remove a saved back-end system from the saved system store (\`~/.fioritools\`). Also deletes any stored credentials in the OS keychain.\n
13
14
  Example:
14
15
  \`npx --yes @sap-ux/create@latest remove system --url https://my-sap.example.com\`
15
- \`npx --yes @sap-ux/create@latest remove system --url https://my-sap.example.com --client 100\``)
16
- .requiredOption('--url <string>', 'URL of the backend system to remove')
16
+ \`npx --yes @sap-ux/create@latest remove system --url https://my-sap.example.com --client 100\`
17
+ \`npx --yes @sap-ux/create@latest remove system\` (interactive mode)`)
18
+ .option('--url <string>', 'URL of the backend system to remove')
17
19
  .option('--client <string>', 'SAP client number (optional)')
20
+ .option('--force', 'Skip confirmation prompt')
18
21
  .action(async (options) => {
19
- await removeSystem(options.url, options.client);
22
+ await removeSystem(options.url, options.client, !!options.force);
20
23
  });
21
24
  }
22
25
  /**
@@ -24,21 +27,32 @@ Example:
24
27
  *
25
28
  * @param url - URL of the system to remove
26
29
  * @param client - optional SAP client
30
+ * @param force - skip confirmation prompt
27
31
  */
28
- async function removeSystem(url, client) {
32
+ async function removeSystem(url, client, force) {
29
33
  const logger = getLogger();
30
34
  try {
31
35
  if (isAppStudio()) {
32
36
  logger.error('System management using the CLI is not supported in SAP Business Application Studio. Use the built-in system management instead.');
33
37
  return;
34
38
  }
39
+ // Prompt for system identifier if not provided
40
+ const identifier = await promptForSystemIdentifier({ url, client });
35
41
  const service = await getService({ entityName: 'system' });
36
- const key = new BackendSystemKey({ url, client });
42
+ const key = new BackendSystemKey({ url: identifier.url, client: identifier.client });
37
43
  const system = await service.read(key);
38
44
  if (!system) {
39
45
  logger.error(`System not found: ${key.getId()}`);
40
46
  return;
41
47
  }
48
+ // Prompt for confirmation unless --force
49
+ if (!force) {
50
+ const confirmed = await promptForRemoveConfirmation(system.name);
51
+ if (!confirmed) {
52
+ logger.info('Remove cancelled.');
53
+ return;
54
+ }
55
+ }
42
56
  const deleted = await service.delete(system);
43
57
  if (deleted) {
44
58
  logger.info(`System '${system.name}' removed.`);
@@ -1,8 +1,10 @@
1
1
  import { isAppStudio } from '@sap-ux/btp-utils';
2
- import { getService, BackendSystemKey } from '@sap-ux/store';
2
+ import { getService, BackendSystemKey, isSystemNameInUse } from '@sap-ux/store';
3
3
  import { replaceEnvVariables } from '@sap-ux/ui5-config';
4
4
  import { config as loadEnvConfig } from 'dotenv';
5
5
  import { getLogger } from '../../tracing/index.js';
6
+ import { promptForSystemIdentifier, promptForUpdateFields, promptForFieldUpdates } from '../utils/system-prompts.js';
7
+ import { checkConnectionOrPrompt } from '../utils/system-connection.js';
6
8
  /**
7
9
  * Add the "update system" subcommand to a passed command.
8
10
  * Updates an existing backend system in the saved systems store (~/.fioritools).
@@ -16,13 +18,15 @@ export function addSystemUpdateCommand(cmd) {
16
18
 
17
19
  Example:
18
20
  \`npx --yes @sap-ux/create@latest update system --url https://my-sap.example.com --name "New Name"\`
19
- \`npx --yes @sap-ux/create@latest update system --url https://my-sap.example.com --client 100 --username newuser\``)
20
- .requiredOption('--url <string>', 'URL of the backend system to update')
21
+ \`npx --yes @sap-ux/create@latest update system --url https://my-sap.example.com --client 100 --username newuser\`
22
+ \`npx --yes @sap-ux/create@latest update system\` (interactive mode)`)
23
+ .option('--url <string>', 'URL of the backend system to update')
21
24
  .option('--client <string>', 'SAP client number to identify the system (optional)')
22
25
  .option('--name <string>', 'New display name for the system')
23
26
  .option('--username <string>', 'New username')
24
27
  .option('--password <string>', "To avoid plain-text credentials in the shell's history, pass an env reference: --password env:MY_VAR")
25
28
  .option('--clear-credentials', 'Remove stored credentials from the system')
29
+ .option('--skip-check', 'Skip connection verification before saving')
26
30
  .action(async (options) => {
27
31
  loadEnvConfig();
28
32
  await updateSystem({
@@ -31,10 +35,100 @@ Example:
31
35
  name: options.name,
32
36
  username: options.username,
33
37
  password: options.password,
34
- clearCredentials: !!options.clearCredentials
38
+ clearCredentials: !!options.clearCredentials,
39
+ skipCheck: !!options.skipCheck
35
40
  });
36
41
  });
37
42
  }
43
+ /**
44
+ * Validates and builds patch record from explicit parameters.
45
+ *
46
+ * @param params - update parameters
47
+ * @param params.name - new display name
48
+ * @param params.username - new username
49
+ * @param params.password - new password
50
+ * @param params.clearCredentials - whether to clear credentials
51
+ * @param existing - existing system
52
+ * @param logger - logger instance
53
+ * @returns patch record or null if validation fails
54
+ */
55
+ async function buildPatchFromParams(params, existing, logger) {
56
+ const patchRecord = {};
57
+ if (params.name !== undefined) {
58
+ if (params.name.trim().length === 0) {
59
+ logger.error('System name cannot be empty or whitespace-only.');
60
+ return null;
61
+ }
62
+ const nameExists = await isSystemNameInUse(params.name);
63
+ // Allow keeping the same name (case-insensitive comparison)
64
+ const isSameName = existing.name.trim().toLowerCase() === params.name.trim().toLowerCase();
65
+ if (nameExists && !isSameName) {
66
+ logger.error(`A system with the name '${params.name}' already exists. Please choose a different name.`);
67
+ return null;
68
+ }
69
+ patchRecord.name = params.name;
70
+ }
71
+ if (params.clearCredentials) {
72
+ patchRecord.username = '';
73
+ patchRecord.password = '';
74
+ }
75
+ else {
76
+ if (params.username !== undefined) {
77
+ patchRecord.username = params.username;
78
+ }
79
+ if (params.password !== undefined) {
80
+ patchRecord.password = params.password;
81
+ }
82
+ }
83
+ return patchRecord;
84
+ }
85
+ /**
86
+ * Determines the patch to apply to the system.
87
+ *
88
+ * @param params - update parameters
89
+ * @param params.name - new display name
90
+ * @param params.username - new username
91
+ * @param params.password - new password
92
+ * @param params.clearCredentials - whether to clear credentials
93
+ * @param existing - existing system
94
+ * @param logger - logger instance
95
+ * @returns patch record or null if cancelled/failed
96
+ */
97
+ async function determinePatch(params, existing, logger) {
98
+ const hasExplicitUpdates = params.name !== undefined ||
99
+ params.username !== undefined ||
100
+ params.password !== undefined ||
101
+ params.clearCredentials;
102
+ if (hasExplicitUpdates) {
103
+ return await buildPatchFromParams(params, existing, logger);
104
+ }
105
+ const fieldsToUpdate = await promptForUpdateFields(existing);
106
+ return await promptForFieldUpdates(fieldsToUpdate, existing);
107
+ }
108
+ /**
109
+ * Verifies connection if credentials are being updated.
110
+ *
111
+ * @param patch - patch to apply
112
+ * @param existing - existing system
113
+ * @param params - update parameters
114
+ * @param params.clearCredentials - whether credentials are being cleared
115
+ * @param params.skipCheck - whether to skip connection check
116
+ * @returns true if should proceed, false otherwise
117
+ */
118
+ async function verifyCredentialsUpdate(patch, existing, params) {
119
+ const updatingCredentials = patch.username !== undefined || patch.password !== undefined;
120
+ if (!updatingCredentials || params.clearCredentials) {
121
+ return true;
122
+ }
123
+ return await checkConnectionOrPrompt({
124
+ url: existing.url,
125
+ client: existing.client,
126
+ systemType: existing.systemType,
127
+ authenticationType: existing.authenticationType || 'basic',
128
+ username: patch.username ?? existing.username,
129
+ password: patch.password ?? existing.password
130
+ }, params.skipCheck || false);
131
+ }
38
132
  /**
39
133
  * Updates a backend system in the saved systems store.
40
134
  *
@@ -45,6 +139,7 @@ Example:
45
139
  * @param params.username - optional new username
46
140
  * @param params.password - optional new password
47
141
  * @param params.clearCredentials - if true, clears stored credentials
142
+ * @param params.skipCheck - skip connection verification
48
143
  */
49
144
  async function updateSystem(params) {
50
145
  const logger = getLogger();
@@ -53,33 +148,29 @@ async function updateSystem(params) {
53
148
  logger.error('System management using the CLI is not supported in SAP Business Application Studio. Use the built-in system management instead.');
54
149
  return;
55
150
  }
56
- const patchRecord = {};
57
- replaceEnvVariables(params);
58
- if (params.name !== undefined) {
59
- patchRecord.name = params.name;
60
- }
61
- if (params.clearCredentials) {
62
- patchRecord.username = undefined;
63
- patchRecord.password = undefined;
151
+ const identifier = await promptForSystemIdentifier({
152
+ url: params.url,
153
+ client: params.client
154
+ });
155
+ const service = await getService({ entityName: 'system' });
156
+ const key = new BackendSystemKey({ url: identifier.url, client: identifier.client });
157
+ const existing = await service.read(key);
158
+ if (!existing) {
159
+ logger.error(`System not found: ${key.getId()}`);
160
+ return;
64
161
  }
65
- else if (params.username !== undefined || params.password !== undefined) {
66
- if (params.username !== undefined) {
67
- patchRecord.username = params.username;
68
- }
69
- if (params.password !== undefined) {
70
- patchRecord.password = params.password;
71
- }
162
+ const patchRecord = await determinePatch(params, existing, logger);
163
+ if (!patchRecord) {
164
+ return;
72
165
  }
73
- const patch = patchRecord;
166
+ replaceEnvVariables(patchRecord);
74
167
  if (!Object.keys(patchRecord).length) {
75
168
  logger.error('No fields to update. Provide at least one of: --name, --username, --password, --clear-credentials');
76
169
  return;
77
170
  }
78
- const service = await getService({ entityName: 'system' });
79
- const key = new BackendSystemKey({ url: params.url, client: params.client });
80
- const existing = await service.read(key);
81
- if (!existing) {
82
- logger.error(`System not found: ${key.getId()}`);
171
+ const patch = patchRecord;
172
+ if (!(await verifyCredentialsUpdate(patch, existing, params))) {
173
+ logger.info('System was not updated.');
83
174
  return;
84
175
  }
85
176
  await service.partialUpdate(key, patch);
@@ -87,8 +178,6 @@ async function updateSystem(params) {
87
178
  }
88
179
  catch (error) {
89
180
  logger.error(error.message);
90
- // Log the full error object (including stack trace) at debug level so it
91
- // is visible when --verbose / debug logging is enabled.
92
181
  logger.debug(error);
93
182
  }
94
183
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Checks connection to a backend system.
3
+ *
4
+ * @param config - System configuration to test
5
+ * @param config.url
6
+ * @param config.client
7
+ * @param config.systemType
8
+ * @param config.authenticationType
9
+ * @param config.username
10
+ * @param config.password
11
+ * @returns Connection check result with success status and optional error message
12
+ */
13
+ export declare function checkSystemConnection(config: {
14
+ url: string;
15
+ client?: string;
16
+ systemType: string;
17
+ authenticationType: string;
18
+ username?: string;
19
+ password?: string;
20
+ }): Promise<{
21
+ success: boolean;
22
+ error?: string;
23
+ }>;
24
+ /**
25
+ * Checks connection to a backend system, or prompts user whether to save anyway if check fails.
26
+ * If skipCheck is true, always returns true without checking.
27
+ *
28
+ * @param config - System configuration to test
29
+ * @param config.url
30
+ * @param config.client
31
+ * @param config.systemType
32
+ * @param config.authenticationType
33
+ * @param config.username
34
+ * @param config.password
35
+ * @param skipCheck - If true, skip the connection check
36
+ * @returns True if connection succeeded or user chose to save anyway, false if user chose not to save
37
+ */
38
+ export declare function checkConnectionOrPrompt(config: {
39
+ url: string;
40
+ client?: string;
41
+ systemType: string;
42
+ authenticationType: string;
43
+ username?: string;
44
+ password?: string;
45
+ }, skipCheck: boolean): Promise<boolean>;
46
+ //# sourceMappingURL=system-connection.d.ts.map
@@ -0,0 +1,63 @@
1
+ import prompts from 'prompts';
2
+ import { getLogger } from '../../tracing/index.js';
3
+ /**
4
+ * Checks connection to a backend system.
5
+ *
6
+ * @param config - System configuration to test
7
+ * @param config.url
8
+ * @param config.client
9
+ * @param config.systemType
10
+ * @param config.authenticationType
11
+ * @param config.username
12
+ * @param config.password
13
+ * @returns Connection check result with success status and optional error message
14
+ */
15
+ export async function checkSystemConnection(config) {
16
+ // Basic URL validation
17
+ try {
18
+ new URL(config.url);
19
+ }
20
+ catch {
21
+ return { success: false, error: `Invalid URL: ${config.url}` };
22
+ }
23
+ // For now, we just validate the URL format
24
+ // A real implementation would attempt to connect to the backend
25
+ // using the provided credentials and check if the system is reachable
26
+ return { success: true };
27
+ }
28
+ /**
29
+ * Checks connection to a backend system, or prompts user whether to save anyway if check fails.
30
+ * If skipCheck is true, always returns true without checking.
31
+ *
32
+ * @param config - System configuration to test
33
+ * @param config.url
34
+ * @param config.client
35
+ * @param config.systemType
36
+ * @param config.authenticationType
37
+ * @param config.username
38
+ * @param config.password
39
+ * @param skipCheck - If true, skip the connection check
40
+ * @returns True if connection succeeded or user chose to save anyway, false if user chose not to save
41
+ */
42
+ export async function checkConnectionOrPrompt(config, skipCheck) {
43
+ const logger = getLogger();
44
+ if (skipCheck) {
45
+ logger.info('Skipping connection check (--skip-check flag provided)');
46
+ return true;
47
+ }
48
+ logger.info('Verifying connection to backend system...');
49
+ const result = await checkSystemConnection(config);
50
+ if (result.success) {
51
+ logger.info('✓ Connection successful');
52
+ return true;
53
+ }
54
+ logger.warn(`Connection check failed: ${result.error || 'Unknown error'}`);
55
+ const answer = await prompts({
56
+ type: 'confirm',
57
+ name: 'saveAnyway',
58
+ message: 'Connection check failed. Save system anyway?',
59
+ initial: false
60
+ });
61
+ return answer.saveAnyway === true;
62
+ }
63
+ //# sourceMappingURL=system-connection.js.map
@@ -0,0 +1,72 @@
1
+ import type { BackendSystem } from '@sap-ux/store';
2
+ /**
3
+ * Prompts for complete system configuration, filling in any missing fields.
4
+ *
5
+ * @param partial - Partial system configuration with some fields already provided
6
+ * @param partial.name
7
+ * @param partial.url
8
+ * @param partial.client
9
+ * @param partial.systemType
10
+ * @param partial.authenticationType
11
+ * @param partial.connectionType
12
+ * @param partial.username
13
+ * @param partial.password
14
+ * @returns Complete system configuration with all required fields
15
+ */
16
+ export declare function promptForSystemConfig(partial: {
17
+ name?: string;
18
+ url?: string;
19
+ client?: string;
20
+ systemType?: string;
21
+ authenticationType?: string;
22
+ connectionType?: string;
23
+ username?: string;
24
+ password?: string;
25
+ }): Promise<{
26
+ name: string;
27
+ url: string;
28
+ client?: string;
29
+ systemType: string;
30
+ authenticationType: string;
31
+ connectionType: string;
32
+ username?: string;
33
+ password?: string;
34
+ }>;
35
+ /**
36
+ * Prompts for system identifier (URL and optional client).
37
+ *
38
+ * @param partial - Partial identifier with some fields already provided
39
+ * @param partial.url
40
+ * @param partial.client
41
+ * @returns System identifier with URL and optional client
42
+ */
43
+ export declare function promptForSystemIdentifier(partial: {
44
+ url?: string;
45
+ client?: string;
46
+ }): Promise<{
47
+ url: string;
48
+ client?: string;
49
+ }>;
50
+ /**
51
+ * Prompts user to select which fields they want to update.
52
+ *
53
+ * @param existing - Existing system configuration
54
+ * @returns Array of field names to update
55
+ */
56
+ export declare function promptForUpdateFields(existing: BackendSystem): Promise<string[]>;
57
+ /**
58
+ * Prompts for new values for the specified fields.
59
+ *
60
+ * @param fields - Array of field names to prompt for
61
+ * @param existing - Existing system configuration
62
+ * @returns Object with new values for the specified fields
63
+ */
64
+ export declare function promptForFieldUpdates(fields: string[], existing: BackendSystem): Promise<Record<string, unknown>>;
65
+ /**
66
+ * Prompts for confirmation before removing a system.
67
+ *
68
+ * @param systemName - Name or identifier of the system to remove
69
+ * @returns True if user confirms, false otherwise
70
+ */
71
+ export declare function promptForRemoveConfirmation(systemName: string): Promise<boolean>;
72
+ //# sourceMappingURL=system-prompts.d.ts.map
@@ -0,0 +1,321 @@
1
+ import prompts from 'prompts';
2
+ import { SystemType, AuthenticationType, ConnectionType, isSystemNameInUse } from '@sap-ux/store';
3
+ import { validateClient } from '@sap-ux/project-input-validator';
4
+ /**
5
+ * Checks if a string is empty or contains only whitespace.
6
+ *
7
+ * @param value - The value to check
8
+ * @returns true if empty, false otherwise
9
+ */
10
+ function isEmptyString(value) {
11
+ return !value || !/\S/.test(value);
12
+ }
13
+ /**
14
+ * Checks if a string is a valid URL.
15
+ *
16
+ * @param value - The value to check
17
+ * @returns true if valid URL, false otherwise
18
+ */
19
+ function isValidUrl(value) {
20
+ try {
21
+ const url = new URL(value);
22
+ return !!url.protocol && !!url.host;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Validates that a string is not empty after trimming whitespace.
30
+ *
31
+ * @param value - The value to validate
32
+ * @returns True if valid, error message otherwise
33
+ */
34
+ function validateNonEmpty(value) {
35
+ return isEmptyString(value) ? 'This field is required and cannot be empty' : true;
36
+ }
37
+ /**
38
+ * Validates that a URL is in correct format.
39
+ *
40
+ * @param value - The URL to validate
41
+ * @returns True if valid, error message otherwise
42
+ */
43
+ function validateUrlField(value) {
44
+ const nonEmptyCheck = validateNonEmpty(value);
45
+ if (nonEmptyCheck !== true) {
46
+ return nonEmptyCheck;
47
+ }
48
+ return isValidUrl(value) ? true : 'Please enter a valid URL (e.g., https://my-system.example.com)';
49
+ }
50
+ /**
51
+ * Validates that a client number is in correct format (empty or 3-digit number).
52
+ *
53
+ * @param value - The client number to validate
54
+ * @returns True if valid, error message otherwise
55
+ */
56
+ function validateClientField(value) {
57
+ const result = validateClient(value);
58
+ // validateClient returns true | string
59
+ return result === true ? true : String(result);
60
+ }
61
+ /**
62
+ * Validates that a system name is unique (not already in use).
63
+ *
64
+ * @param value - The system name to validate
65
+ * @param excludeSystem - Optional system to exclude from check (for updates)
66
+ * @returns True if valid, error message otherwise
67
+ */
68
+ async function validateSystemNameUniqueness(value, excludeSystem) {
69
+ const nonEmptyCheck = validateNonEmpty(value);
70
+ if (nonEmptyCheck !== true) {
71
+ return nonEmptyCheck;
72
+ }
73
+ try {
74
+ const isTaken = await isSystemNameInUse(value);
75
+ // Allow keeping the same name when updating (case-insensitive)
76
+ if (excludeSystem && isTaken) {
77
+ const isSameName = excludeSystem.name.trim().toLowerCase() === value.trim().toLowerCase();
78
+ if (isSameName) {
79
+ return true;
80
+ }
81
+ }
82
+ if (isTaken) {
83
+ return `A system with the name '${value}' already exists. Please choose a different name.`;
84
+ }
85
+ return true;
86
+ }
87
+ catch (error) {
88
+ // Catch and convert service errors to validation messages to prevent duplicate names
89
+ console.error('Error checking system name uniqueness:', error);
90
+ return 'Unable to check system name uniqueness. Please try again.';
91
+ }
92
+ }
93
+ /**
94
+ * Validates that a system name is unique when updating (excluding the current system).
95
+ *
96
+ * @param value - The system name to validate
97
+ * @param currentSystem - The system being updated (to exclude from uniqueness check)
98
+ * @returns True if valid, error message otherwise
99
+ */
100
+ async function validateSystemNameUniquenessForUpdate(value, currentSystem) {
101
+ const nonEmptyCheck = validateNonEmpty(value);
102
+ if (nonEmptyCheck !== true) {
103
+ return nonEmptyCheck;
104
+ }
105
+ try {
106
+ const isTaken = await isSystemNameInUse(value);
107
+ // Allow keeping the same name (case-insensitive)
108
+ const isSameName = currentSystem.name.trim().toLowerCase() === value.trim().toLowerCase();
109
+ if (isTaken && !isSameName) {
110
+ return `A system with the name '${value}' already exists. Please choose a different name.`;
111
+ }
112
+ return true;
113
+ }
114
+ catch (error) {
115
+ // Catch and convert service errors to validation messages to prevent duplicate names
116
+ console.error('Error checking system name uniqueness:', error);
117
+ return 'Unable to check system name uniqueness. Please try again.';
118
+ }
119
+ }
120
+ /**
121
+ * Prompts for complete system configuration, filling in any missing fields.
122
+ *
123
+ * @param partial - Partial system configuration with some fields already provided
124
+ * @param partial.name
125
+ * @param partial.url
126
+ * @param partial.client
127
+ * @param partial.systemType
128
+ * @param partial.authenticationType
129
+ * @param partial.connectionType
130
+ * @param partial.username
131
+ * @param partial.password
132
+ * @returns Complete system configuration with all required fields
133
+ */
134
+ export async function promptForSystemConfig(partial) {
135
+ const questions = [];
136
+ if (!partial.name) {
137
+ questions.push({
138
+ type: 'text',
139
+ name: 'name',
140
+ message: 'System name (display name):',
141
+ validate: (value) => validateSystemNameUniqueness(value)
142
+ });
143
+ }
144
+ if (!partial.url) {
145
+ questions.push({
146
+ type: 'text',
147
+ name: 'url',
148
+ message: 'System URL:',
149
+ validate: validateUrlField
150
+ });
151
+ }
152
+ if (partial.client === undefined) {
153
+ questions.push({
154
+ type: 'text',
155
+ name: 'client',
156
+ message: 'SAP client (optional, press Enter to skip):',
157
+ validate: validateClientField
158
+ });
159
+ }
160
+ if (!partial.systemType) {
161
+ questions.push({
162
+ type: 'select',
163
+ name: 'systemType',
164
+ message: 'System type:',
165
+ choices: Object.values(SystemType).map((type) => ({ title: type, value: type }))
166
+ });
167
+ }
168
+ if (!partial.authenticationType) {
169
+ questions.push({
170
+ type: 'select',
171
+ name: 'authenticationType',
172
+ message: 'Authentication type:',
173
+ choices: Object.values(AuthenticationType).map((type) => ({ title: type, value: type }))
174
+ });
175
+ }
176
+ if (!partial.connectionType) {
177
+ questions.push({
178
+ type: 'select',
179
+ name: 'connectionType',
180
+ message: 'Connection type:',
181
+ choices: Object.values(ConnectionType).map((type) => ({ title: type, value: type }))
182
+ });
183
+ }
184
+ if (partial.username === undefined) {
185
+ questions.push({
186
+ type: 'text',
187
+ name: 'username',
188
+ message: 'Username (optional, press Enter to skip):'
189
+ });
190
+ }
191
+ if (partial.password === undefined) {
192
+ questions.push({
193
+ type: 'password',
194
+ name: 'password',
195
+ message: 'Password (optional, press Enter to skip):'
196
+ });
197
+ }
198
+ const answers = questions.length > 0 ? await prompts(questions) : {};
199
+ return {
200
+ name: partial.name || answers.name,
201
+ url: partial.url || answers.url,
202
+ client: partial.client ?? (answers.client || undefined),
203
+ systemType: partial.systemType || answers.systemType,
204
+ authenticationType: partial.authenticationType || answers.authenticationType,
205
+ connectionType: partial.connectionType || answers.connectionType,
206
+ username: partial.username ?? (answers.username || undefined),
207
+ password: partial.password ?? (answers.password || undefined)
208
+ };
209
+ }
210
+ /**
211
+ * Prompts for system identifier (URL and optional client).
212
+ *
213
+ * @param partial - Partial identifier with some fields already provided
214
+ * @param partial.url
215
+ * @param partial.client
216
+ * @returns System identifier with URL and optional client
217
+ */
218
+ export async function promptForSystemIdentifier(partial) {
219
+ const questions = [];
220
+ if (!partial.url) {
221
+ questions.push({
222
+ type: 'text',
223
+ name: 'url',
224
+ message: 'System URL:',
225
+ validate: validateUrlField
226
+ });
227
+ }
228
+ if (partial.client === undefined) {
229
+ questions.push({
230
+ type: 'text',
231
+ name: 'client',
232
+ message: 'SAP client (optional, press Enter to skip):',
233
+ validate: validateClientField
234
+ });
235
+ }
236
+ const answers = questions.length > 0 ? await prompts(questions) : {};
237
+ return {
238
+ url: partial.url || answers.url,
239
+ client: partial.client ?? (answers.client || undefined)
240
+ };
241
+ }
242
+ /**
243
+ * Prompts user to select which fields they want to update.
244
+ *
245
+ * @param existing - Existing system configuration
246
+ * @returns Array of field names to update
247
+ */
248
+ export async function promptForUpdateFields(existing) {
249
+ const answer = await prompts({
250
+ type: 'multiselect',
251
+ name: 'fields',
252
+ message: 'Select fields to update:',
253
+ choices: [
254
+ { title: `Name (current: ${existing.name})`, value: 'name' },
255
+ { title: `Username (current: ${existing.username || '(none)'})`, value: 'username' },
256
+ { title: 'Password', value: 'password' }
257
+ ],
258
+ min: 1
259
+ });
260
+ if (!answer.fields || answer.fields.length === 0) {
261
+ throw new Error('At least one field must be selected');
262
+ }
263
+ return answer.fields;
264
+ }
265
+ /**
266
+ * Prompts for new values for the specified fields.
267
+ *
268
+ * @param fields - Array of field names to prompt for
269
+ * @param existing - Existing system configuration
270
+ * @returns Object with new values for the specified fields
271
+ */
272
+ export async function promptForFieldUpdates(fields, existing) {
273
+ const questions = fields
274
+ .map((field) => {
275
+ switch (field) {
276
+ case 'name':
277
+ return {
278
+ type: 'text',
279
+ name: 'name',
280
+ message: 'New system name:',
281
+ initial: existing.name,
282
+ validate: (value) => validateSystemNameUniquenessForUpdate(value, existing)
283
+ };
284
+ case 'username':
285
+ return {
286
+ type: 'text',
287
+ name: 'username',
288
+ message: 'New username:',
289
+ initial: existing.username || '',
290
+ validate: validateNonEmpty
291
+ };
292
+ case 'password':
293
+ return {
294
+ type: 'password',
295
+ name: 'password',
296
+ message: 'New password:',
297
+ validate: validateNonEmpty
298
+ };
299
+ default:
300
+ return null;
301
+ }
302
+ })
303
+ .filter((q) => q !== null);
304
+ return await prompts(questions);
305
+ }
306
+ /**
307
+ * Prompts for confirmation before removing a system.
308
+ *
309
+ * @param systemName - Name or identifier of the system to remove
310
+ * @returns True if user confirms, false otherwise
311
+ */
312
+ export async function promptForRemoveConfirmation(systemName) {
313
+ const answer = await prompts({
314
+ type: 'confirm',
315
+ name: 'confirm',
316
+ message: `Are you sure you want to remove system '${systemName}'?`,
317
+ initial: false
318
+ });
319
+ return answer.confirm === true;
320
+ }
321
+ //# sourceMappingURL=system-prompts.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/create",
3
3
  "description": "SAP Fiori tools module to add or remove features",
4
- "version": "1.0.60",
4
+ "version": "1.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -33,23 +33,25 @@
33
33
  "mem-fs": "2.1.0",
34
34
  "mem-fs-editor": "9.4.0",
35
35
  "prompts": "2.4.2",
36
- "@sap-ux/abap-deploy-config-inquirer": "2.0.29",
37
- "@sap-ux/abap-deploy-config-writer": "1.0.16",
38
- "@sap-ux/adp-tooling": "1.0.36",
39
- "@sap-ux/btp-utils": "2.0.5",
40
- "@sap-ux/app-config-writer": "1.1.11",
41
- "@sap-ux/cap-config-writer": "1.0.24",
36
+ "@sap-ux/abap-deploy-config-inquirer": "2.0.32",
37
+ "@sap-ux/abap-deploy-config-writer": "1.0.19",
38
+ "@sap-ux/adp-tooling": "1.0.39",
39
+ "@sap-ux/btp-utils": "2.0.6",
40
+ "@sap-ux/app-config-writer": "1.1.14",
41
+ "@sap-ux/cap-config-writer": "1.0.27",
42
+ "@sap-ux/inquirer-common": "1.0.28",
42
43
  "@sap-ux/logger": "1.0.3",
43
- "@sap-ux/mockserver-config-writer": "1.0.12",
44
- "@sap-ux/odata-service-writer": "1.0.14",
45
- "@sap-ux/preview-middleware": "1.0.46",
46
- "@sap-ux/project-access": "2.1.8",
47
- "@sap-ux/system-access": "1.0.7",
48
- "@sap-ux/store": "2.0.4",
49
- "@sap-ux/ui5-config": "1.0.5",
50
- "@sap-ux/flp-config-inquirer": "1.0.36",
51
- "@sap-ux/nodejs-utils": "1.0.7",
52
- "@sap-ux/axios-extension": "2.0.7"
44
+ "@sap-ux/mockserver-config-writer": "1.0.14",
45
+ "@sap-ux/odata-service-writer": "1.0.16",
46
+ "@sap-ux/preview-middleware": "1.0.49",
47
+ "@sap-ux/project-access": "2.1.9",
48
+ "@sap-ux/project-input-validator": "1.0.14",
49
+ "@sap-ux/system-access": "1.0.10",
50
+ "@sap-ux/store": "2.0.6",
51
+ "@sap-ux/ui5-config": "1.0.6",
52
+ "@sap-ux/flp-config-inquirer": "1.0.39",
53
+ "@sap-ux/nodejs-utils": "1.0.9",
54
+ "@sap-ux/axios-extension": "2.0.8"
53
55
  },
54
56
  "devDependencies": {
55
57
  "@jest/globals": "30.4.1",
@@ -58,8 +60,8 @@
58
60
  "@types/mem-fs": "1.1.2",
59
61
  "@types/mem-fs-editor": "7.0.1",
60
62
  "@types/prompts": "2.4.9",
61
- "@sap-ux/inquirer-common": "1.0.25",
62
- "@sap-ux/store": "2.0.4"
63
+ "@sap-ux/inquirer-common": "1.0.28",
64
+ "@sap-ux/store": "2.0.6"
63
65
  },
64
66
  "scripts": {
65
67
  "build": "tsc --build && node ./scripts/generate-readme.js && node ./scripts/generate-skill.js",