@pacaf/wizard-ux 3.6.5 → 3.6.7
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pacaf/wizard-ux",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser-based setup wizard for Power Apps Code Apps.",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"react-dom": "^19.0.0",
|
|
39
39
|
"react-resizable-panels": "^2.1.7",
|
|
40
40
|
"react-router-dom": "^7.1.0",
|
|
41
|
-
"@pacaf/wizard": "3.4.
|
|
41
|
+
"@pacaf/wizard": "3.4.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.0",
|
|
@@ -151,6 +151,36 @@ export function selectDuplicateProfiles(profiles, keepName) {
|
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Select every profile that must be removed before we finalize the discovery
|
|
156
|
+
* profile into its environment-scoped name. Two categories are unsafe:
|
|
157
|
+
*
|
|
158
|
+
* 1. Same-user + same-kind duplicates of the profile we are keeping. Once the
|
|
159
|
+
* discovery profile is retargeted onto Dev it shares the (user, env, tenant)
|
|
160
|
+
* key with these, which is what trips pac's SingleOrDefault crash (#102).
|
|
161
|
+
* 2. Any profile whose name already equals the target name we are about to
|
|
162
|
+
* rename to. `pac auth name` cannot rename onto a name that is already
|
|
163
|
+
* taken, and a leftover SPN/user profile from a prior run for the same Dev
|
|
164
|
+
* environment collides here even though its user/kind differ.
|
|
165
|
+
*
|
|
166
|
+
* Pure (no I/O) so the selection rules can be unit-tested against fixtures.
|
|
167
|
+
*/
|
|
168
|
+
export function selectProfilesToPrune(profiles, keepName, targetName) {
|
|
169
|
+
const keep = profiles.find((p) => p.name === keepName);
|
|
170
|
+
const out = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const p of profiles) {
|
|
173
|
+
if (p.name === keepName) continue;
|
|
174
|
+
const isUserKindDup = keep && p.user === keep.user && p.kind === keep.kind;
|
|
175
|
+
const isTargetCollision = targetName && p.name === targetName;
|
|
176
|
+
if ((isUserKindDup || isTargetCollision) && !seen.has(p.name)) {
|
|
177
|
+
seen.add(p.name);
|
|
178
|
+
out.push(p);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
/**
|
|
155
185
|
* Detect the signature of the corrupted-auth-store crash so we can surface an
|
|
156
186
|
* actionable remediation instead of the raw pac stack trace. The
|
|
@@ -164,6 +194,25 @@ export function isDuplicateAuthStoreError(output) {
|
|
|
164
194
|
|| /InvalidOperationException/i.test(s);
|
|
165
195
|
}
|
|
166
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Extract the most useful error text from a runSafeCapture result. pac writes
|
|
199
|
+
* its real error lines to STDOUT (e.g. "Error: The value 2 of --index is not
|
|
200
|
+
* valid..."), while runSafeCapture puts Node's generic "Command failed: ..."
|
|
201
|
+
* into `stderr` (from err.message). Reading stderr-first therefore shadows the
|
|
202
|
+
* real reason — so prefer an `Error:` line from stdout, then stdout, then
|
|
203
|
+
* stderr. Returns scrubbed text safe for display.
|
|
204
|
+
*/
|
|
205
|
+
export function extractPacError(result) {
|
|
206
|
+
const stdout = String(result?.stdout || '');
|
|
207
|
+
const stderr = String(result?.stderr || '');
|
|
208
|
+
const errLine = stdout
|
|
209
|
+
.split(/\r?\n/)
|
|
210
|
+
.map((l) => l.trim())
|
|
211
|
+
.find((l) => /^Error:/i.test(l));
|
|
212
|
+
const detail = errLine || stdout.trim() || stderr.trim() || '';
|
|
213
|
+
return SCRUB.scrubSecrets(detail);
|
|
214
|
+
}
|
|
215
|
+
|
|
167
216
|
/** Fetch and parse `pac auth list` into structured rows. */
|
|
168
217
|
function parseAuthProfiles(pac) {
|
|
169
218
|
const res = SHELL.runSafeCapture(pac, ['auth', 'list']);
|
|
@@ -172,30 +221,37 @@ function parseAuthProfiles(pac) {
|
|
|
172
221
|
}
|
|
173
222
|
|
|
174
223
|
/**
|
|
175
|
-
* Pre-flight auth hygiene
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
224
|
+
* Pre-flight auth hygiene, run BEFORE the discovery profile is retargeted onto
|
|
225
|
+
* Dev. `pac auth name`/`delete` crash with "Sequence contains more than one
|
|
226
|
+
* matching element" when the local auth store holds two profiles that resolve
|
|
227
|
+
* to the same (user, environment, tenant) key — a known pac CLI bug whose
|
|
228
|
+
* AuthProfiles.Update/Delete uses SingleOrDefault.
|
|
229
|
+
*
|
|
230
|
+
* Two leftovers from prior wizard runs trigger this:
|
|
231
|
+
* - same-user/kind duplicates of the discovery profile, and
|
|
232
|
+
* - a profile already named exactly `targetName` (the env-scoped name we are
|
|
233
|
+
* about to rename onto).
|
|
180
234
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
235
|
+
* Crucially this must happen while keys are still DISTINCT — i.e. before
|
|
236
|
+
* `pac env select` points the discovery profile at Dev. Removing them now
|
|
237
|
+
* prevents the ambiguous store from ever forming, which is the actual root
|
|
238
|
+
* cause behind the recurring "Could not finalize the auth profile" failure.
|
|
239
|
+
*
|
|
240
|
+
* Returns the number of profiles removed.
|
|
186
241
|
*/
|
|
187
|
-
function
|
|
242
|
+
function pruneConflictingAuthProfiles(log, pac, keepName, targetName) {
|
|
188
243
|
const profiles = parseAuthProfiles(pac);
|
|
189
|
-
if (profiles.length === 0) return;
|
|
190
|
-
const dupes =
|
|
191
|
-
if (dupes.length === 0) return;
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
if (profiles.length === 0) return 0;
|
|
245
|
+
const dupes = selectProfilesToPrune(profiles, keepName, targetName);
|
|
246
|
+
if (dupes.length === 0) return 0;
|
|
247
|
+
log.warn(`Found ${dupes.length} conflicting auth profile(s) from a prior run; removing them to avoid a known pac CLI crash.`);
|
|
248
|
+
let removed = 0;
|
|
194
249
|
for (const d of dupes) {
|
|
195
250
|
const del = SHELL.runSafeCapture(pac, ['auth', 'delete', '--name', d.name]);
|
|
196
|
-
if (del.ok) log.ok(`Removed stale auth profile ${d.name}`);
|
|
251
|
+
if (del.ok) { log.ok(`Removed stale auth profile ${d.name}`); removed += 1; }
|
|
197
252
|
else log.warn(`Could not remove stale auth profile ${d.name}. If finalize still fails, run \`pac auth clear\` and retry.`);
|
|
198
253
|
}
|
|
254
|
+
return removed;
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
export default {
|
|
@@ -333,30 +389,58 @@ export default {
|
|
|
333
389
|
|
|
334
390
|
const discoveryIdx = findProfileIndexByName(pac, DISCOVERY_PROFILE_NAME);
|
|
335
391
|
if (discoveryIdx != null) {
|
|
392
|
+
// Pre-flight hygiene FIRST, while auth-store keys are still distinct.
|
|
393
|
+
// Removing same-user/kind duplicates and any existing profile already
|
|
394
|
+
// named `userProfileName` BEFORE we retarget the discovery profile onto
|
|
395
|
+
// Dev prevents the ambiguous (user, env, tenant) store that crashes both
|
|
396
|
+
// `pac auth delete` and `pac auth name` (issues #102 and the recurring
|
|
397
|
+
// "Could not finalize the auth profile" failure). Doing this after
|
|
398
|
+
// `env select` is the original defect — by then the keys already collide.
|
|
399
|
+
pruneConflictingAuthProfiles(log, pac, DISCOVERY_PROFILE_NAME, userProfileName);
|
|
400
|
+
|
|
336
401
|
// Retarget the (already-authenticated) discovery profile to Dev, then
|
|
337
402
|
// rename it to the environment-scoped name — no second sign-in.
|
|
338
403
|
SHELL.runSafeCapture(pac, ['auth', 'select', '--name', DISCOVERY_PROFILE_NAME]);
|
|
339
404
|
const sel = SHELL.runSafeCapture(pac, ['env', 'select', '--environment', devUrl]);
|
|
340
|
-
if (!sel.ok) throw new Error(`Could not target ${devUrl}: ${
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
const renameIdx = findProfileIndexByName(pac, DISCOVERY_PROFILE_NAME)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
405
|
+
if (!sel.ok) throw new Error(`Could not target ${devUrl}: ${extractPacError(sel)}`);
|
|
406
|
+
|
|
407
|
+
// Re-resolve the discovery profile's index against the CURRENT store —
|
|
408
|
+
// never fall back to the pre-prune index, which may now be out of range.
|
|
409
|
+
const renameIdx = findProfileIndexByName(pac, DISCOVERY_PROFILE_NAME);
|
|
410
|
+
if (renameIdx == null) {
|
|
411
|
+
// Idempotent recovery: an interrupted prior run may have already
|
|
412
|
+
// renamed discovery into userProfileName. Accept that and move on.
|
|
413
|
+
if (findProfileIndexByName(pac, userProfileName) != null) {
|
|
414
|
+
SHELL.runSafeCapture(pac, ['auth', 'select', '--name', userProfileName]);
|
|
415
|
+
SHELL.runSafeCapture(pac, ['env', 'select', '--environment', devUrl]);
|
|
416
|
+
log.ok(`Reusing existing profile ${userProfileName} (targeting Dev).`);
|
|
417
|
+
} else {
|
|
349
418
|
throw new Error([
|
|
350
|
-
'Could not finalize the auth profile:
|
|
351
|
-
'This
|
|
352
|
-
'Fix it by clearing the local profiles, then click Save & run again:',
|
|
419
|
+
'Could not finalize the auth profile: the sign-in profile disappeared from the local PAC auth store.',
|
|
420
|
+
'This usually means the store still holds conflicting profiles. Clear them, then click Save & run again:',
|
|
353
421
|
' pac auth clear',
|
|
354
422
|
'(After clearing you will be asked to sign in once more to recreate a single clean profile.)',
|
|
355
423
|
].join('\n'));
|
|
356
424
|
}
|
|
357
|
-
|
|
425
|
+
} else {
|
|
426
|
+
const renamed = SHELL.runSafeCapture(pac, ['auth', 'name', '--index', String(renameIdx), '--name', userProfileName]);
|
|
427
|
+
// pac writes errors to stdout; check both the result flag and the store.
|
|
428
|
+
const finalized = renamed.ok || findProfileIndexByName(pac, userProfileName) != null;
|
|
429
|
+
if (!finalized) {
|
|
430
|
+
const detail = extractPacError(renamed);
|
|
431
|
+
if (isDuplicateAuthStoreError(detail)) {
|
|
432
|
+
throw new Error([
|
|
433
|
+
'Could not finalize the auth profile: your local PAC auth store has duplicate or dual-active profiles.',
|
|
434
|
+
'This is a known pac CLI bug ("Sequence contains more than one matching element") that crashes `pac auth name`.',
|
|
435
|
+
'Fix it by clearing the local profiles, then click Save & run again:',
|
|
436
|
+
' pac auth clear',
|
|
437
|
+
'(After clearing you will be asked to sign in once more to recreate a single clean profile.)',
|
|
438
|
+
].join('\n'));
|
|
439
|
+
}
|
|
440
|
+
throw new Error(`Could not finalize the auth profile: ${detail}`);
|
|
441
|
+
}
|
|
442
|
+
log.ok(`Profile ${userProfileName} ready (targeting Dev).`);
|
|
358
443
|
}
|
|
359
|
-
log.ok(`Profile ${userProfileName} ready (targeting Dev).`);
|
|
360
444
|
} else if (findProfileIndexByName(pac, userProfileName) != null) {
|
|
361
445
|
// Re-run after a previous success: discovery was already renamed.
|
|
362
446
|
SHELL.runSafeCapture(pac, ['auth', 'select', '--name', userProfileName]);
|
|
@@ -3,6 +3,8 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import {
|
|
4
4
|
parseAuthListOutput,
|
|
5
5
|
selectDuplicateProfiles,
|
|
6
|
+
selectProfilesToPrune,
|
|
7
|
+
extractPacError,
|
|
6
8
|
isDuplicateAuthStoreError,
|
|
7
9
|
} from './05-environments.mjs';
|
|
8
10
|
|
|
@@ -55,3 +57,56 @@ test('isDuplicateAuthStoreError matches the known pac crash signatures', () => {
|
|
|
55
57
|
assert.equal(isDuplicateAuthStoreError('Error: environment not found'), false);
|
|
56
58
|
assert.equal(isDuplicateAuthStoreError(''), false);
|
|
57
59
|
});
|
|
60
|
+
|
|
61
|
+
// Reproduces the exact log from the recurring failure: a leftover env-scoped
|
|
62
|
+
// user profile from a prior run already holds the target name. Same user as
|
|
63
|
+
// discovery so it is also a same-user duplicate.
|
|
64
|
+
const TARGET_COLLISION_OUTPUT = `
|
|
65
|
+
Index Active Kind Name User
|
|
66
|
+
[1] * UNIVERSAL pp-prosp8dc-d-u-6ae3eeaf user@contoso.com User
|
|
67
|
+
[2] UNIVERSAL pacaf-discovery user@contoso.com User
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
test('selectProfilesToPrune removes a profile already holding the target name', () => {
|
|
71
|
+
const profiles = parseAuthListOutput(TARGET_COLLISION_OUTPUT);
|
|
72
|
+
const prune = selectProfilesToPrune(profiles, 'pacaf-discovery', 'pp-prosp8dc-d-u-6ae3eeaf');
|
|
73
|
+
assert.deepEqual(prune.map((p) => p.name), ['pp-prosp8dc-d-u-6ae3eeaf']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('selectProfilesToPrune removes same-user/kind dupes plus a distinct target collision', () => {
|
|
77
|
+
const output = `
|
|
78
|
+
[1] * UNIVERSAL pacaf-discovery me@contoso.com User
|
|
79
|
+
[2] UNIVERSAL pp-dup-d-u-aaaa me@contoso.com User
|
|
80
|
+
[3] UNIVERSAL pp-target-d-u-bb someone@else.com User
|
|
81
|
+
`;
|
|
82
|
+
const prune = selectProfilesToPrune(parseAuthListOutput(output), 'pacaf-discovery', 'pp-target-d-u-bb');
|
|
83
|
+
assert.deepEqual(prune.map((p) => p.name).sort(), ['pp-dup-d-u-aaaa', 'pp-target-d-u-bb']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('selectProfilesToPrune does not double-list a profile that is both dup and target', () => {
|
|
87
|
+
const profiles = parseAuthListOutput(TARGET_COLLISION_OUTPUT);
|
|
88
|
+
const prune = selectProfilesToPrune(profiles, 'pacaf-discovery', 'pp-prosp8dc-d-u-6ae3eeaf');
|
|
89
|
+
assert.equal(prune.length, 1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('selectProfilesToPrune never returns the kept profile', () => {
|
|
93
|
+
const profiles = parseAuthListOutput(TARGET_COLLISION_OUTPUT);
|
|
94
|
+
const prune = selectProfilesToPrune(profiles, 'pacaf-discovery', 'pacaf-discovery');
|
|
95
|
+
assert.equal(prune.find((p) => p.name === 'pacaf-discovery'), undefined);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('extractPacError prefers the pac Error: line from stdout over the generic stderr', () => {
|
|
99
|
+
// pac writes the real reason to stdout; runSafeCapture puts "Command failed"
|
|
100
|
+
// into stderr. Reading stderr-first (the old bug) would shadow the real error.
|
|
101
|
+
const result = {
|
|
102
|
+
ok: false,
|
|
103
|
+
stdout: 'Index Active Kind ...\n\nError: The value 2 of --index is not valid. It must be between 1 and 1.\n',
|
|
104
|
+
stderr: 'Command failed: pac auth name --index 2 --name foo',
|
|
105
|
+
};
|
|
106
|
+
assert.equal(extractPacError(result), 'Error: The value 2 of --index is not valid. It must be between 1 and 1.');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('extractPacError falls back to stderr when stdout is empty', () => {
|
|
110
|
+
assert.equal(extractPacError({ ok: false, stdout: '', stderr: 'boom' }), 'boom');
|
|
111
|
+
assert.equal(extractPacError({}), '');
|
|
112
|
+
});
|