@pacaf/wizard-ux 3.5.1 → 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.
- package/dist/assets/{index-BL1yODjj.js → index-COjPOI_E.js} +11 -11
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/lib/pac-parse.mjs +55 -0
- package/server/steps/01-prerequisites.mjs +133 -2
- package/server/steps/02-project-and-env.mjs +10 -73
- package/server/steps/04-auth-setup.mjs +42 -286
- package/server/steps/05-environments.mjs +436 -0
- package/server/steps/{05-publisher.mjs → 06-publisher.mjs} +9 -57
- package/server/steps/{06-solution.mjs → 07-solution.mjs} +8 -8
- package/server/steps/{07-scaffold.mjs → 08-scaffold.mjs} +8 -8
- package/server/steps/{08-connectors.mjs → 09-connectors.mjs} +13 -13
- package/server/steps/{09-verify-deploy.mjs → 10-verify-deploy.mjs} +6 -6
- package/server/steps/{10-add-to-solution.mjs → 11-add-to-solution.mjs} +2 -2
- package/server/steps/index.mjs +8 -7
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
// Step 4 -
|
|
2
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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: '
|
|
149
|
-
description: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
176
|
+
stateUpdate: { WIZARD_DISCOVERY_PROFILE: DISCOVERY_PROFILE_NAME },
|
|
328
177
|
completedStep: 4,
|
|
329
178
|
};
|
|
330
179
|
}
|
|
331
180
|
|
|
332
|
-
// ── Service Principal flow
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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:
|
|
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
|
},
|