@pacaf/wizard-ux 3.5.2 → 3.6.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.
@@ -1,21 +1,24 @@
1
- // Step 4 - Auth Setup. Browser-native credential persistence and PAC profile setup.
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, chmodSync } from 'node:fs';
1
+ // Step 4 - Sign In. Authenticate at the tenant level (no environment URL yet) so
2
+ // Step 5 can discover environments via `pac env list`. Environment-scoped PAC
3
+ // profiles and .env files are created in Step 5 once the environment is chosen.
3
4
  import { execFileSync } from 'node:child_process';
4
- import { dirname, join, resolve } from 'node:path';
5
+ import { dirname, resolve } from 'node:path';
5
6
  import { platform } from 'node:os';
6
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
- import { getSecret, hasUsableSecret, recoverSecret, setSecret, persistSecretToCache, clearSecretCache } from '../lib/dataverse-bridge.mjs';
8
+ import { getSecret, hasUsableSecret, recoverSecret, setSecret, persistSecretToCache } from '../lib/dataverse-bridge.mjs';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  // PACKAGE_DIR locates sibling @pacaf/wizard lib files (must stay __dirname-relative).
11
- // PROJECT_DIR is the user's working directory (profile names, env files, git hooks, cwd).
12
+ // PROJECT_DIR is the user's working directory (cwd for interactive sign-in).
12
13
  const PACKAGE_DIR = resolve(__dirname, '..', '..', '..');
13
14
  const PROJECT_DIR = process.cwd();
14
15
  const SHELL = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'shell.mjs')).href);
15
- const CRYPTO = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'crypto.mjs')).href);
16
- const PAC_TARGET = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'pac-target.mjs')).href);
17
16
  const SCRUB = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'scrub.mjs')).href);
18
17
 
18
+ // Transient tenant-level profile created here purely so Step 5 can run
19
+ // `pac env list`. Step 5 renames it (user) or replaces it (SPN).
20
+ const DISCOVERY_PROFILE_NAME = 'pacaf-discovery';
21
+
19
22
  function hasCommand(name) {
20
23
  try {
21
24
  execFileSync(platform() === 'win32' ? 'where' : 'which', [name], { stdio: 'ignore' });
@@ -25,29 +28,6 @@ function hasCommand(name) {
25
28
  }
26
29
  }
27
30
 
28
- /**
29
- * Remove any PP_CLIENT_SECRET=... line from .env.local. Called when the
30
- * user picks 1Password storage so an encrypted blob from a previous run
31
- * doesn't linger inside the (possibly cloud-synced) project folder.
32
- */
33
- function removeSecretFromEnvLocal() {
34
- const envLocalPath = join(PROJECT_DIR, '.env.local');
35
- if (!existsSync(envLocalPath)) return;
36
- const original = readFileSync(envLocalPath, 'utf-8');
37
- if (!/^PP_CLIENT_SECRET=/m.test(original)) return;
38
- const cleaned = original.replace(/^PP_CLIENT_SECRET=.*\r?\n?/m, '');
39
- writeFileSync(envLocalPath, cleaned, 'utf-8');
40
- }
41
-
42
- function normalizeUrl(value) {
43
- return String(value || '').trim().replace(/\/+$/, '').toLowerCase();
44
- }
45
-
46
- function warnOnUrlDrift(log, key, stateUrl, credentialUrl) {
47
- if (!credentialUrl || normalizeUrl(stateUrl) === normalizeUrl(credentialUrl)) return;
48
- log.warn(`${key} in your credential source (${credentialUrl}) differs from wizard state (${stateUrl}). Update the credential source so downstream scripts stay in sync.`);
49
- }
50
-
51
31
  function formatPacAuthCreateError(profileName, url, output = '') {
52
32
  const scrubbed = SCRUB.scrubSecrets(String(output || ''));
53
33
  const lines = scrubbed.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
@@ -59,54 +39,6 @@ function formatPacAuthCreateError(profileName, url, output = '') {
59
39
  ].join('\n');
60
40
  }
61
41
 
62
- function createPacProfile(log, pac, targetKey, url, appId, secret, tenantId) {
63
- const profileName = PAC_TARGET.buildPacProfileName({ rootDir: PROJECT_DIR, targetKey, profileType: 'spn', url });
64
- const result = SHELL.runSafeCapture(pac, [
65
- 'auth', 'create', '--name', profileName, '--environment', url,
66
- '--applicationId', appId, '--clientSecret', secret, '--tenant', tenantId,
67
- ]);
68
- if (!result.ok) throw new Error(formatPacAuthCreateError(profileName, url, result.stderr || result.stdout));
69
- log.ok(`${profileName} profile created`);
70
- }
71
-
72
- function createProfiles(log, pac, credentialValues, state) {
73
- const tenantId = credentialValues.PP_TENANT_ID;
74
- const clientId = credentialValues.PP_APP_ID;
75
- const secret = credentialValues.PP_CLIENT_SECRET;
76
- const devUrl = String(state.PP_ENV_DEV || '').trim();
77
- const testUrl = String(state.PP_ENV_TEST || '').trim();
78
- const prodUrl = String(state.PP_ENV_PROD || '').trim();
79
- if (!tenantId || !clientId || !secret || !devUrl) {
80
- throw new Error('Resolved credentials are incomplete. Expected tenant ID, app ID, client secret, and Dev environment URL.');
81
- }
82
- warnOnUrlDrift(log, 'PP_ENV_DEV', devUrl, credentialValues.PP_ENV_DEV);
83
- if (testUrl) warnOnUrlDrift(log, 'PP_ENV_TEST', testUrl, credentialValues.PP_ENV_TEST);
84
- if (prodUrl) warnOnUrlDrift(log, 'PP_ENV_PROD', prodUrl, credentialValues.PP_ENV_PROD);
85
- log.info('Creating PAC SPN auth profiles...');
86
- createPacProfile(log, pac, 'dev', devUrl, clientId, secret, tenantId);
87
- if (testUrl) createPacProfile(log, pac, 'test', testUrl, clientId, secret, tenantId);
88
- if (prodUrl) createPacProfile(log, pac, 'prod', prodUrl, clientId, secret, tenantId);
89
- }
90
-
91
- function installPreCommitHook(log) {
92
- const hookDir = join(PROJECT_DIR, '.git', 'hooks');
93
- const hookPath = join(hookDir, 'pre-commit');
94
- const hookSource = join(PROJECT_DIR, 'scripts', 'pre-commit-hook.sh');
95
- if (!existsSync(join(PROJECT_DIR, '.git')) || !existsSync(hookSource)) return;
96
- if (existsSync(hookPath)) {
97
- const existing = readFileSync(hookPath, 'utf-8');
98
- if (!existing.includes('papps-secret-guard')) return;
99
- }
100
- try {
101
- mkdirSync(hookDir, { recursive: true });
102
- copyFileSync(hookSource, hookPath);
103
- if (platform() !== 'win32') chmodSync(hookPath, 0o755);
104
- log.ok('Installed pre-commit hook');
105
- } catch {
106
- log.warn('Could not install pre-commit hook. Install scripts/pre-commit-hook.sh manually if desired.');
107
- }
108
- }
109
-
110
42
  function runLivePac(log, pac, args, opts = {}) {
111
43
  return new Promise((resolvePromise) => {
112
44
  log.info(`$ ${SCRUB.scrubSecrets(SHELL.formatCommandForLog(pac, args))}`);
@@ -145,8 +77,8 @@ function runLivePac(log, pac, args, opts = {}) {
145
77
  export default {
146
78
  meta: {
147
79
  number: 4,
148
- title: 'PAC Auth Profiles',
149
- description: 'The PAC CLI needs its own auth profiles to communicate with your Power Platform environment. This step creates those profiles using the credentials from Step 3, writes configuration files, and verifies the connection works.',
80
+ title: 'Sign In',
81
+ description: 'Sign in to Power Platform once. The next step uses this sign-in to list your environments so you can pick Dev, Test, and Prod no URLs to paste.',
150
82
  canRunInBrowser: true,
151
83
  },
152
84
 
@@ -196,7 +128,6 @@ export default {
196
128
  ...(hasOp ? [{ value: '1password', label: '1Password references in .env' }] : []),
197
129
  { value: 'envlocal', label: '.env.local file (encrypted secret, gitignored)' },
198
130
  ],
199
- help: state.AUTH_MODE ? `Currently set to ${state.AUTH_MODE} from Step 3.` : undefined,
200
131
  },
201
132
  {
202
133
  id: 'OP_VAULT',
@@ -204,7 +135,6 @@ export default {
204
135
  label: '1Password vault name',
205
136
  defaultValue: state.OP_VAULT || 'Engineering',
206
137
  hideIf: [{ id: 'AUTH_MODE', equals: 'envlocal' }],
207
- help: state.OP_VAULT ? `Using "${state.OP_VAULT}" from Step 3.` : undefined,
208
138
  },
209
139
  {
210
140
  id: 'OP_ITEM',
@@ -212,25 +142,6 @@ export default {
212
142
  label: '1Password item name',
213
143
  defaultValue: state.OP_ITEM || `PowerApps CodeApps - ${state.APP_NAME || 'My App'}`,
214
144
  hideIf: [{ id: 'AUTH_MODE', equals: 'envlocal' }],
215
- help: state.OP_ITEM ? `Using "${state.OP_ITEM}" from Step 3.` : undefined,
216
- },
217
- {
218
- id: 'CREATE_USER_PROFILE',
219
- type: 'confirm',
220
- label: 'Create the interactive user auth profile now',
221
- help: 'Required once for pac code run, pac code push, and pac code add-data-source.',
222
- defaultValue: true,
223
- },
224
- {
225
- id: 'USER_SIGN_IN_METHOD',
226
- type: 'select',
227
- label: 'User sign-in method',
228
- defaultValue: 'deviceCode',
229
- options: [
230
- { value: 'deviceCode', label: 'Device code — most reliable' },
231
- { value: 'browser', label: 'Browser — fastest when callback works' },
232
- ],
233
- hideIf: { id: 'CREATE_USER_PROFILE', equals: false },
234
145
  },
235
146
  );
236
147
  return questions;
@@ -238,223 +149,68 @@ export default {
238
149
 
239
150
  async apply(answers, state, log) {
240
151
  const pac = SHELL.pacPath();
241
- if (!pac) throw new Error('PAC CLI was not found. Install it before creating auth profiles.');
152
+ if (!pac) throw new Error('PAC CLI was not found. Install it before signing in.');
242
153
 
243
154
  const authProfileType = state.AUTH_PROFILE_TYPE || 'user';
244
- const devUrl = String(state.PP_ENV_DEV || '').trim();
245
- const testUrl = String(state.PP_ENV_TEST || '').trim();
246
- const prodUrl = String(state.PP_ENV_PROD || '').trim();
247
- const dateStr = new Date().toISOString().slice(0, 10);
248
- const targetKey = state.WIZARD_TARGET_ENV || 'dev';
249
155
 
250
- if (!devUrl) throw new Error('Dev environment URL must be set (Step 2) before auth setup.');
156
+ // Remove any stale discovery profile from a previous run so the create below
157
+ // doesn't fail with "profile already exists".
158
+ SHELL.runSafeCapture(pac, ['auth', 'delete', '--name', DISCOVERY_PROFILE_NAME]);
251
159
 
252
160
  // ── User credentials flow ──
253
161
  if (authProfileType === 'user') {
254
162
  const method = answers.USER_SIGN_IN_METHOD === 'browser' ? 'browser' : 'deviceCode';
255
- const userProfileName = PAC_TARGET.buildPacProfileName({ rootDir: PROJECT_DIR, targetKey, profileType: 'user', url: devUrl });
256
-
257
- log.info('Creating user auth profile via interactive sign-in...');
258
- const userArgs = ['auth', 'create', '--name', userProfileName, '--environment', devUrl];
259
- if (method === 'deviceCode') userArgs.push('--deviceCode');
163
+ log.info('Signing in interactively (tenant level)...');
164
+ const args = ['auth', 'create', '--name', DISCOVERY_PROFILE_NAME];
165
+ if (method === 'deviceCode') args.push('--deviceCode');
260
166
 
261
- let userOk = await runLivePac(log, pac, userArgs, { timeoutMs: method === 'browser' ? 180000 : 0 });
262
-
263
- // Fallback to device code if browser timed out
264
- if (!userOk && method === 'browser') {
167
+ let ok = await runLivePac(log, pac, args, { timeoutMs: method === 'browser' ? 180000 : 0 });
168
+ if (!ok && method === 'browser') {
265
169
  log.warn('Browser sign-in did not complete. Falling back to device code...');
266
- userOk = await runLivePac(log, pac, [
267
- 'auth', 'create', '--name', userProfileName, '--environment', devUrl, '--deviceCode',
268
- ]);
170
+ ok = await runLivePac(log, pac, ['auth', 'create', '--name', DISCOVERY_PROFILE_NAME, '--deviceCode']);
269
171
  }
172
+ if (!ok) throw new Error('Sign-in did not complete. Re-run this step and finish the device-code or browser prompt.');
270
173
 
271
- if (!userOk) throw new Error(`Could not create user profile. Run later: ${pac} auth create --name ${userProfileName} --environment ${devUrl} --deviceCode`);
272
- log.ok(`User profile ${userProfileName} created`);
273
-
274
- // Write .env — with op:// references if 1Password, or plain URLs otherwise
275
- const authMode = state.AUTH_MODE || '';
276
- if (authMode === '1password' && state.OP_VAULT && state.OP_ITEM) {
277
- const vault = state.OP_VAULT;
278
- const itemName = state.OP_ITEM;
279
- // 1Password mode: ensure no encrypted PP_CLIENT_SECRET lingers in .env.local
280
- // and remove the out-of-tree secret cache.
281
- try { removeSecretFromEnvLocal(); } catch { /* best-effort */ }
282
- try { clearSecretCache(); } catch { /* best-effort */ }
283
- const envContent = [
284
- '# .env - Safe to commit. Contains 1Password references, not secrets.',
285
- `# Generated by setup wizard on ${dateStr}`,
286
- '',
287
- `PP_ENV_DEV=op://${vault}/${itemName}/env-dev`,
288
- testUrl ? `PP_ENV_TEST=op://${vault}/${itemName}/env-test` : '',
289
- prodUrl ? `PP_ENV_PROD=op://${vault}/${itemName}/env-prod` : '',
290
- '',
291
- ].filter((line) => line !== '').join('\n');
292
- writeFileSync(join(PROJECT_DIR, '.env'), `${envContent}\n`, 'utf-8');
293
- log.ok('Wrote .env with 1Password references');
294
- } else {
295
- const envContent = [
296
- '# .env - Environment URLs for Power Platform.',
297
- `# Generated by setup wizard on ${dateStr}`,
298
- '',
299
- `PP_ENV_DEV=${devUrl}`,
300
- testUrl ? `PP_ENV_TEST=${testUrl}` : '',
301
- prodUrl ? `PP_ENV_PROD=${prodUrl}` : '',
302
- '',
303
- ].filter((line) => line !== '').join('\n');
304
- writeFileSync(join(PROJECT_DIR, '.env'), `${envContent}\n`, 'utf-8');
305
- log.ok('Wrote .env with environment URLs');
306
- }
307
-
308
- installPreCommitHook(log);
309
-
310
- // Verify the user profile
311
- const verification = PAC_TARGET.selectAndVerifyPacProfile({
312
- pac,
313
- rootDir: PROJECT_DIR,
314
- wizardState: { WIZARD_TARGET_ENV: targetKey, PP_ENV_DEV: devUrl, PP_ENV_TEST: testUrl, PP_ENV_PROD: prodUrl },
315
- targetKey,
316
- profileType: 'user',
317
- credentialValues: null,
318
- powerConfigPath: join(PROJECT_DIR, 'power.config.json'),
319
- requireCredentialMatch: false,
320
- requirePowerConfig: false,
321
- requirePowerConfigTarget: false,
322
- });
323
- log.ok(`Verified ${verification.profileName}`);
324
- log.info(verification.whoInfo.raw.split('\n').map((line) => ` ${line}`).join('\n'));
325
-
174
+ log.ok('Signed in. Pick your environments in the next step.');
326
175
  return {
327
- stateUpdate: { AUTH_MODE: authMode },
176
+ stateUpdate: { WIZARD_DISCOVERY_PROFILE: DISCOVERY_PROFILE_NAME },
328
177
  completedStep: 4,
329
178
  };
330
179
  }
331
180
 
332
- // ── Service Principal flow (existing behavior) ──
181
+ // ── Service Principal flow ──
333
182
  const enteredSecret = String(answers.PP_CLIENT_SECRET || '').trim();
334
183
  if (enteredSecret) setSecret(enteredSecret);
335
184
  const secret = getSecret() || recoverSecret();
336
185
  if (!secret) throw new Error('Client secret is required. Enter it in this step or complete Step 3 first.');
337
186
 
338
- const authMode = String(answers.AUTH_MODE || state.AUTH_MODE || 'envlocal');
339
187
  const tenantId = String(state.PP_TENANT_ID || '').trim();
340
188
  const clientId = String(state.PP_APP_ID || '').trim();
189
+ if (!tenantId || !clientId) throw new Error('Step 3 must be complete with Tenant ID and Client ID before signing in.');
341
190
 
342
- if (!tenantId || !clientId) throw new Error('Step 3 must be complete with Tenant ID and Client ID before auth setup.');
343
-
344
- if (authMode === '1password') {
345
- const vault = String(answers.OP_VAULT || state.OP_VAULT || '').trim();
346
- const itemName = String(answers.OP_ITEM || state.OP_ITEM || '').trim();
347
- if (!hasCommand('op')) throw new Error('1Password storage was selected, but op CLI is not available.');
348
- if (!vault || !itemName) throw new Error('1Password vault and item name are required.');
349
- // 1Password mode: scrub any prior PP_CLIENT_SECRET line and OS-temp cache.
350
- try { removeSecretFromEnvLocal(); } catch { /* best-effort */ }
351
- try { clearSecretCache(); } catch { /* best-effort */ }
352
- const envContent = [
353
- '# .env - Safe to commit. Contains 1Password references, not secrets.',
354
- `# Generated by setup wizard on ${dateStr}`,
355
- '',
356
- `PP_TENANT_ID=op://${vault}/${itemName}/tenant-id`,
357
- `PP_APP_ID=op://${vault}/${itemName}/app-id`,
358
- `PP_CLIENT_SECRET=op://${vault}/${itemName}/client-secret`,
359
- `PP_ENV_DEV=op://${vault}/${itemName}/env-dev`,
360
- testUrl ? `PP_ENV_TEST=op://${vault}/${itemName}/env-test` : '',
361
- prodUrl ? `PP_ENV_PROD=op://${vault}/${itemName}/env-prod` : '',
362
- '',
363
- ].filter((line) => line !== '').join('\n');
364
- writeFileSync(join(PROJECT_DIR, '.env'), `${envContent}\n`, 'utf-8');
365
- log.ok('Wrote .env with 1Password references');
366
- } else {
367
- const gitignorePath = join(PROJECT_DIR, '.gitignore');
368
- if (existsSync(gitignorePath)) {
369
- const gitignore = readFileSync(gitignorePath, 'utf-8');
370
- if (!gitignore.includes('.env.local')) appendFileSync(gitignorePath, '\n.env.local\n');
371
- } else {
372
- writeFileSync(gitignorePath, '.env.local\n', 'utf-8');
373
- }
374
- const envLocalContent = [
375
- '# .env.local - DO NOT commit to Git.',
376
- `# Generated by setup wizard on ${dateStr}`,
377
- '',
378
- `PP_TENANT_ID=${tenantId}`,
379
- `PP_APP_ID=${clientId}`,
380
- `PP_CLIENT_SECRET=${CRYPTO.encrypt(secret)}`,
381
- `PP_ENV_DEV=${devUrl}`,
382
- testUrl ? `PP_ENV_TEST=${testUrl}` : '',
383
- prodUrl ? `PP_ENV_PROD=${prodUrl}` : '',
384
- '',
385
- ].filter((line) => line !== '').join('\n');
386
- writeFileSync(join(PROJECT_DIR, '.env.local'), `${envLocalContent}\n`, 'utf-8');
387
- if (platform() !== 'win32') {
388
- try { chmodSync(join(PROJECT_DIR, '.env.local'), 0o600); } catch { /* best effort */ }
389
- }
390
- // Mirror the encrypted secret to the OS-temp cache so a wizard restart
391
- // can recover without prompting (and without touching .env.local).
392
- try { persistSecretToCache(secret); } catch { /* best-effort */ }
393
- log.ok('Wrote .env.local');
191
+ const authMode = String(answers.AUTH_MODE || state.AUTH_MODE || 'envlocal');
192
+ if (authMode === '1password' && !hasCommand('op')) {
193
+ throw new Error('1Password storage was selected, but the op CLI is not available.');
394
194
  }
395
195
 
396
- const opBin = hasCommand('op') ? 'op' : null;
397
- const credentialValues = PAC_TARGET.resolveCredentialValues({ rootDir: PROJECT_DIR, opBin, source: authMode });
398
- createProfiles(log, pac, credentialValues, state);
399
- installPreCommitHook(log);
400
-
401
- const envTemplate = [
402
- '# .env.template - Copy to .env.local and fill in values',
403
- '# DO NOT commit .env.local to Git',
404
- '',
405
- 'PP_TENANT_ID=',
406
- 'PP_APP_ID=',
407
- 'PP_CLIENT_SECRET=',
408
- `PP_ENV_DEV=${devUrl}`,
409
- testUrl ? `PP_ENV_TEST=${testUrl}` : '',
410
- prodUrl ? `PP_ENV_PROD=${prodUrl}` : '',
411
- '',
412
- ].filter((line) => line !== '').join('\n');
413
- writeFileSync(join(PROJECT_DIR, '.env.template'), `${envTemplate}\n`, 'utf-8');
414
- log.ok('Wrote .env.template');
196
+ // Carry the secret to Step 5, where .env.local / env-scoped profiles are written.
197
+ try { persistSecretToCache(secret); } catch { /* best-effort */ }
415
198
 
416
- const wizardState = {
417
- WIZARD_TARGET_ENV: targetKey,
418
- PP_ENV_DEV: devUrl,
419
- PP_ENV_TEST: testUrl,
420
- PP_ENV_PROD: prodUrl,
421
- };
422
- const verification = PAC_TARGET.selectAndVerifyPacProfile({
423
- pac,
424
- rootDir: PROJECT_DIR,
425
- wizardState,
426
- targetKey,
427
- profileType: 'spn',
428
- credentialValues,
429
- powerConfigPath: join(PROJECT_DIR, 'power.config.json'),
430
- requireCredentialMatch: true,
431
- requirePowerConfig: false,
432
- requirePowerConfigTarget: false,
433
- });
434
- log.ok(`Verified ${verification.profileName}`);
435
- log.info(verification.whoInfo.raw.split('\n').map((line) => ` ${line}`).join('\n'));
436
-
437
- if (answers.CREATE_USER_PROFILE === true) {
438
- const userProfileName = PAC_TARGET.buildPacProfileName({ rootDir: PROJECT_DIR, targetKey, profileType: 'user', url: devUrl });
439
- const method = answers.USER_SIGN_IN_METHOD === 'browser' ? 'browser' : 'deviceCode';
440
- log.warn('PAC code commands require a user auth profile. Starting user sign-in now.');
441
- const userArgs = ['auth', 'create', '--name', userProfileName, '--environment', devUrl];
442
- if (method === 'deviceCode') userArgs.push('--deviceCode');
443
- const userOk = await runLivePac(log, pac, userArgs, { timeoutMs: method === 'browser' ? 180000 : 0 });
444
- if (userOk) {
445
- log.ok(`User profile ${userProfileName} created`);
446
- const spnProfileName = PAC_TARGET.buildPacProfileName({ rootDir: PROJECT_DIR, targetKey, profileType: 'spn', url: devUrl });
447
- SHELL.runSafeCapture(pac, ['auth', 'select', '--name', spnProfileName]);
448
- log.ok('Switched back to SPN profile for the next setup steps');
449
- } else {
450
- log.warn(`Could not create user profile. Run later: ${pac} auth create --name ${userProfileName} --environment ${devUrl} --deviceCode`);
451
- }
199
+ log.info('Authenticating service principal (tenant level)...');
200
+ const created = SHELL.runSafeCapture(pac, [
201
+ 'auth', 'create', '--name', DISCOVERY_PROFILE_NAME,
202
+ '--applicationId', clientId, '--clientSecret', secret, '--tenant', tenantId,
203
+ ]);
204
+ if (!created.ok) {
205
+ throw new Error(formatPacAuthCreateError(DISCOVERY_PROFILE_NAME, 'your tenant', created.stderr || created.stdout));
452
206
  }
207
+ log.ok('Service principal authenticated. Pick your environments in the next step.');
453
208
 
454
209
  return {
455
210
  stateUpdate: {
211
+ WIZARD_DISCOVERY_PROFILE: DISCOVERY_PROFILE_NAME,
456
212
  AUTH_MODE: authMode,
457
- HAS_OP: opBin !== null,
213
+ HAS_OP: hasCommand('op'),
458
214
  OP_VAULT: authMode === '1password' ? answers.OP_VAULT : state.OP_VAULT,
459
215
  OP_ITEM: authMode === '1password' ? answers.OP_ITEM : state.OP_ITEM,
460
216
  },