@pacaf/wizard-ux 3.0.1 → 3.0.3
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-BVelUveV.js → index-zSgX3W7E.js} +31 -31
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/routes/onepassword.mjs +5 -0
- package/server/steps/03-app-registration.mjs +10 -13
- package/server/steps/05-publisher.mjs +158 -165
- package/server/steps/07-scaffold.mjs +8 -8
package/dist/index.html
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
30
30
|
</style>
|
|
31
|
-
<script type="module" crossorigin src="
|
|
31
|
+
<script type="module" crossorigin src="/assets/index-zSgX3W7E.js"></script>
|
|
32
32
|
</head>
|
|
33
33
|
<body>
|
|
34
34
|
<div id="root"><div id="boot"><div class="ring"></div></div></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pacaf/wizard-ux",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser-based setup wizard for Power Apps Code Apps (parallel to @pacaf/wizard CLI).",
|
|
@@ -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.0
|
|
41
|
+
"@pacaf/wizard": "3.1.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.0",
|
|
@@ -7,6 +7,11 @@ const STEP3_PATH = resolve(__dirname, '..', 'steps', '03-app-registration.mjs');
|
|
|
7
7
|
const step3 = await import(pathToFileURL(STEP3_PATH).href);
|
|
8
8
|
|
|
9
9
|
export default async function onepasswordRoutes(fastify, opts) {
|
|
10
|
+
fastify.get('/vaults', async () => {
|
|
11
|
+
const vaults = step3.listOpVaults();
|
|
12
|
+
return { vaults };
|
|
13
|
+
});
|
|
14
|
+
|
|
10
15
|
fastify.get('/items', async (req) => {
|
|
11
16
|
const vault = String(req.query.vault || '').trim();
|
|
12
17
|
if (!vault) return { items: [] };
|
|
@@ -65,7 +65,7 @@ function listOpItems(vaultName) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Exported for the API route to call
|
|
68
|
-
export { listOpItems };
|
|
68
|
+
export { listOpItems, listOpVaults };
|
|
69
69
|
|
|
70
70
|
function createOpVault(log, name) {
|
|
71
71
|
const result = runSafe('op', ['vault', 'create', name, '--format=json']);
|
|
@@ -145,10 +145,6 @@ export default {
|
|
|
145
145
|
const hasOp = hasCommand('op') || state.HAS_OP === true;
|
|
146
146
|
const currentAuthType = state.AUTH_PROFILE_TYPE || 'user';
|
|
147
147
|
|
|
148
|
-
// ── 1Password vault/item discovery ──
|
|
149
|
-
const vaults = hasOp ? listOpVaults() : [];
|
|
150
|
-
const defaultVault = state.OP_VAULT || vaults[0]?.value || '';
|
|
151
|
-
|
|
152
148
|
return [
|
|
153
149
|
{
|
|
154
150
|
id: 'AUTH_PROFILE_TYPE',
|
|
@@ -170,23 +166,24 @@ export default {
|
|
|
170
166
|
help: hasOp
|
|
171
167
|
? 'Store environment URLs and credentials as 1Password references. Secrets never touch disk.'
|
|
172
168
|
: '1Password CLI was not detected. Leave this off unless op is available in your shell.',
|
|
173
|
-
defaultValue: hasOp && state.AUTH_MODE === '1password',
|
|
169
|
+
defaultValue: state.USE_1PASSWORD === true || (hasOp && state.AUTH_MODE === '1password'),
|
|
174
170
|
},
|
|
175
171
|
{
|
|
176
172
|
id: 'OP_VAULT',
|
|
177
173
|
type: 'select',
|
|
178
174
|
label: '1Password vault',
|
|
179
|
-
help:
|
|
180
|
-
|
|
181
|
-
: 'Could not discover vaults (1Password may be locked or offline). If you just authenticated to 1Password, refresh this page to reload the vault list. Otherwise, enter a vault name manually or create a new one.',
|
|
182
|
-
defaultValue: state.OP_VAULT && vaults.some((v) => v.value === state.OP_VAULT)
|
|
183
|
-
? state.OP_VAULT
|
|
184
|
-
: (vaults[0]?.value || ENTER_MANUALLY),
|
|
175
|
+
help: 'Select an existing vault, create a new one, or enter a name manually. Vaults load when 1Password is enabled — if the list is empty, ensure the op CLI is signed in (`op signin`) and toggle 1Password off and on to retry.',
|
|
176
|
+
defaultValue: state.OP_VAULT || ENTER_MANUALLY,
|
|
185
177
|
options: [
|
|
186
|
-
...vaults,
|
|
187
178
|
{ value: ENTER_MANUALLY, label: 'Enter vault name manually' },
|
|
188
179
|
{ value: CREATE_NEW_VAULT, label: '+ Create new vault' },
|
|
189
180
|
],
|
|
181
|
+
dynamicOptions: {
|
|
182
|
+
endpoint: '/api/1password/vaults',
|
|
183
|
+
param: 'enabled',
|
|
184
|
+
dependsOn: 'USE_1PASSWORD',
|
|
185
|
+
responseKey: 'vaults',
|
|
186
|
+
},
|
|
190
187
|
hideIf: { id: 'USE_1PASSWORD', equals: false },
|
|
191
188
|
},
|
|
192
189
|
{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Step 5 — Solution & Publisher (solution-first, publisher auto-resolved).
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { dirname, resolve, join } from 'node:path';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
3
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
6
|
import { dvGet, dvPost, hasUsableSecret, setSecret, clearSecret } from '../lib/dataverse-bridge.mjs';
|
|
5
7
|
|
|
@@ -12,6 +14,21 @@ const PAC_TARGET = await import(pathToFileURL(resolve(ROOT_DIR, 'wizard', 'lib',
|
|
|
12
14
|
const CREATE_NEW = '__create_new__';
|
|
13
15
|
const PASTE_URL = '__paste_url__';
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Run `pac env fetch` using --xmlFile instead of --xml to avoid XmlException
|
|
19
|
+
* on macOS PAC CLI 2.2.1+ where inline FetchXML attribute quotes get corrupted.
|
|
20
|
+
* Writes the XML to a temp file, runs the command, and cleans up.
|
|
21
|
+
*/
|
|
22
|
+
function pacEnvFetchXml(pac, fetchXml, opts) {
|
|
23
|
+
const tmp = join(tmpdir(), `pac-fetch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.xml`);
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(tmp, fetchXml, 'utf-8');
|
|
26
|
+
return SHELL.runSafe(pac, ['env', 'fetch', '--xmlFile', tmp], opts);
|
|
27
|
+
} finally {
|
|
28
|
+
try { unlinkSync(tmp); } catch { /* best effort cleanup */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
16
33
|
|
|
17
34
|
/** Get the Maker Portal solutions deep-link via `pac org who`. */
|
|
@@ -22,7 +39,7 @@ function getMakerPortalLink() {
|
|
|
22
39
|
if (whoOut) {
|
|
23
40
|
const whoInfo = PAC_TARGET.parsePacOrgWho(whoOut);
|
|
24
41
|
if (whoInfo.environmentId) {
|
|
25
|
-
return `https://make.powerapps.com/
|
|
42
|
+
return `https://make.powerapps.com/environments/${whoInfo.environmentId}/solutions`;
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
45
|
} catch { /* fall through */ }
|
|
@@ -36,58 +53,104 @@ function extractSolutionIdFromUrl(url) {
|
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
56
|
+
* Parse PAC CLI column-aligned tabular output into an array of row objects.
|
|
57
|
+
* Finds the header line (containing known column keywords), determines column
|
|
58
|
+
* boundaries from header token positions, and slices each subsequent data line
|
|
59
|
+
* by those boundaries. Handles multi-word values (e.g. "Climb Tracker") and
|
|
60
|
+
* aliased columns (e.g. "pub.customizationprefix").
|
|
61
|
+
*/
|
|
62
|
+
function parsePacTabularRows(output, headerHints) {
|
|
63
|
+
const allLines = output.split(/\r?\n/);
|
|
64
|
+
const hints = headerHints || ['uniquename', 'solutionid', 'friendlyname'];
|
|
65
|
+
|
|
66
|
+
// Find the header line — first line containing at least one of the hints.
|
|
67
|
+
let headerIdx = -1;
|
|
68
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
69
|
+
const lower = allLines[i].toLowerCase();
|
|
70
|
+
if (hints.some((h) => lower.includes(h))) {
|
|
71
|
+
headerIdx = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (headerIdx < 0) return [];
|
|
76
|
+
|
|
77
|
+
const headerLine = allLines[headerIdx];
|
|
78
|
+
|
|
79
|
+
// Extract column names and their start positions from the header.
|
|
80
|
+
const cols = [];
|
|
81
|
+
const re = /\S+/g;
|
|
82
|
+
let m;
|
|
83
|
+
while ((m = re.exec(headerLine)) !== null) {
|
|
84
|
+
cols.push({ name: m[0].toLowerCase(), start: m.index });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse each data line after the header.
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (let i = headerIdx + 1; i < allLines.length; i++) {
|
|
90
|
+
const line = allLines[i];
|
|
91
|
+
if (!line.trim()) continue;
|
|
92
|
+
if (/^(Connected|Microsoft|Version:|Online|Feedback)/i.test(line.trim())) continue;
|
|
93
|
+
const row = {};
|
|
94
|
+
for (let c = 0; c < cols.length; c++) {
|
|
95
|
+
const start = cols[c].start;
|
|
96
|
+
const end = c < cols.length - 1 ? cols[c + 1].start : line.length;
|
|
97
|
+
row[cols[c].name] = (start < line.length ? line.slice(start, end) : '').trim();
|
|
98
|
+
}
|
|
99
|
+
if (Object.values(row).some((v) => v)) rows.push(row);
|
|
100
|
+
}
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch solution + publisher by solution ID using a single joined FetchXML.
|
|
106
|
+
* Uses <link-entity> to include publisher columns directly, avoiding the
|
|
107
|
+
* two-query approach that broke when publisherid rendered as a display name.
|
|
42
108
|
*/
|
|
43
109
|
function fetchSolutionViaPac(solutionId) {
|
|
44
110
|
const pac = SHELL.pacPath();
|
|
45
111
|
if (!pac) return null;
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
publisherFriendlyName = extractFriendlyName(pubOutput, [pubGuid, prefix, publisherUniqueName, choiceValuePrefix]);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
112
|
+
|
|
113
|
+
const xml = [
|
|
114
|
+
'<fetch>',
|
|
115
|
+
' <entity name="solution">',
|
|
116
|
+
' <attribute name="uniquename"/>',
|
|
117
|
+
' <attribute name="friendlyname"/>',
|
|
118
|
+
' <attribute name="version"/>',
|
|
119
|
+
' <attribute name="solutionid"/>',
|
|
120
|
+
' <link-entity name="publisher" from="publisherid" to="publisherid" link-type="inner" alias="pub">',
|
|
121
|
+
' <attribute name="customizationprefix"/>',
|
|
122
|
+
' <attribute name="uniquename"/>',
|
|
123
|
+
' <attribute name="friendlyname"/>',
|
|
124
|
+
' <attribute name="publisherid"/>',
|
|
125
|
+
' <attribute name="customizationoptionvalueprefix"/>',
|
|
126
|
+
' </link-entity>',
|
|
127
|
+
' <filter>',
|
|
128
|
+
` <condition attribute="solutionid" operator="eq" value="${solutionId}"/>`,
|
|
129
|
+
' </filter>',
|
|
130
|
+
' </entity>',
|
|
131
|
+
'</fetch>',
|
|
132
|
+
].join('\n');
|
|
133
|
+
|
|
134
|
+
const output = pacEnvFetchXml(pac, xml);
|
|
135
|
+
if (!output) return null;
|
|
136
|
+
|
|
137
|
+
const rows = parsePacTabularRows(output, [
|
|
138
|
+
'uniquename', 'friendlyname', 'solutionid', 'pub.customizationprefix',
|
|
139
|
+
]);
|
|
140
|
+
if (rows.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
const row = rows[0];
|
|
143
|
+
|
|
144
|
+
// Column names come back lowercased. Aliased columns use "pub.<attr>".
|
|
145
|
+
const solId = (row['solutionid'] || solutionId).toLowerCase();
|
|
146
|
+
const uniquename = row['uniquename'] || '';
|
|
147
|
+
const friendlyname = row['friendlyname'] || '';
|
|
148
|
+
const version = row['version'] || '';
|
|
149
|
+
const prefix = row['pub.customizationprefix'] || '';
|
|
150
|
+
const pubGuid = row['pub.publisherid'] || '';
|
|
151
|
+
const publisherUniqueName = row['pub.uniquename'] || '';
|
|
152
|
+
const publisherFriendlyName = row['pub.friendlyname'] || '';
|
|
153
|
+
const choiceValuePrefix = row['pub.customizationoptionvalueprefix'] || '';
|
|
91
154
|
|
|
92
155
|
return {
|
|
93
156
|
solutionid: solId,
|
|
@@ -103,130 +166,59 @@ function fetchSolutionViaPac(solutionId) {
|
|
|
103
166
|
}
|
|
104
167
|
|
|
105
168
|
/**
|
|
106
|
-
* List unmanaged solutions via `pac env fetch
|
|
107
|
-
*
|
|
169
|
+
* List unmanaged solutions via a single `pac env fetch` with a publisher join.
|
|
170
|
+
* Returns [{ value: solutionid, label: "Display Name (prefix_)" }].
|
|
171
|
+
* Uses parsePacTabularRows to handle multi-word names correctly and avoids
|
|
172
|
+
* the N+1 per-solution fetch loop.
|
|
108
173
|
*/
|
|
109
174
|
function listSolutionSummariesViaPac() {
|
|
110
175
|
const pac = SHELL.pacPath();
|
|
111
176
|
if (!pac) return [];
|
|
112
|
-
|
|
113
|
-
const
|
|
177
|
+
|
|
178
|
+
const xml = [
|
|
179
|
+
'<fetch>',
|
|
180
|
+
' <entity name="solution">',
|
|
181
|
+
' <attribute name="uniquename"/>',
|
|
182
|
+
' <attribute name="friendlyname"/>',
|
|
183
|
+
' <attribute name="solutionid"/>',
|
|
184
|
+
' <link-entity name="publisher" from="publisherid" to="publisherid" link-type="inner" alias="pub">',
|
|
185
|
+
' <attribute name="customizationprefix"/>',
|
|
186
|
+
' <attribute name="friendlyname"/>',
|
|
187
|
+
' </link-entity>',
|
|
188
|
+
' <filter>',
|
|
189
|
+
' <condition attribute="ismanaged" operator="eq" value="0"/>',
|
|
190
|
+
' <condition attribute="isvisible" operator="eq" value="1"/>',
|
|
191
|
+
' </filter>',
|
|
192
|
+
' <order attribute="friendlyname"/>',
|
|
193
|
+
' </entity>',
|
|
194
|
+
'</fetch>',
|
|
195
|
+
].join('\n');
|
|
196
|
+
|
|
197
|
+
const output = pacEnvFetchXml(pac, xml, { timeout: 15000 });
|
|
114
198
|
if (!output) return [];
|
|
115
199
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.map((m) => m[1].toLowerCase());
|
|
120
|
-
const uniqueGuids = [...new Set(guids)];
|
|
200
|
+
const rows = parsePacTabularRows(output, [
|
|
201
|
+
'uniquename', 'friendlyname', 'solutionid', 'pub.customizationprefix',
|
|
202
|
+
]);
|
|
121
203
|
|
|
122
|
-
// Filter out
|
|
204
|
+
// Filter out system solutions.
|
|
123
205
|
const systemGuids = new Set([
|
|
124
206
|
'fd140aaf-4df4-11dd-bd17-0019b9312238', // Default Solution
|
|
125
207
|
'00000001-0000-0000-0001-00000000009b', // Common Data Services
|
|
126
208
|
]);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// The header line contains column names. Skip it.
|
|
141
|
-
const headerKeywords = ['uniquename', 'friendlyname', 'solutionid'];
|
|
142
|
-
const dataLines = lines.filter((l) => !headerKeywords.some((kw) => l.toLowerCase().includes(kw)));
|
|
143
|
-
const fullData = dataLines.join(' ');
|
|
144
|
-
|
|
145
|
-
const results = [];
|
|
146
|
-
for (const guid of uniqueGuids) {
|
|
147
|
-
if (systemGuids.has(guid)) continue;
|
|
148
|
-
|
|
149
|
-
// Find the text around this GUID to extract a label
|
|
150
|
-
const idx = fullData.indexOf(guid);
|
|
151
|
-
if (idx < 0) continue;
|
|
152
|
-
|
|
153
|
-
// Look for a short lowercase prefix near the GUID (the pub.customizationprefix)
|
|
154
|
-
const nearby = fullData.slice(Math.max(0, idx - 200), idx + 200);
|
|
155
|
-
const prefixMatch = nearby.match(/\b([a-z]{2,8})\b/);
|
|
156
|
-
const prefix = prefixMatch ? prefixMatch[1] : '?';
|
|
157
|
-
|
|
158
|
-
// The friendlyname is harder to extract from the concatenated data.
|
|
159
|
-
// We'll use the GUID as the value and fetch full details on selection.
|
|
160
|
-
results.push({ value: guid, label: `Solution ${guid.slice(0, 8)}… (${prefix}_)` });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// If we got results but labels are poor, try fetching details for each
|
|
164
|
-
// (only if there are few solutions)
|
|
165
|
-
if (results.length > 0 && results.length <= 20) {
|
|
166
|
-
for (const r of results) {
|
|
167
|
-
const details = fetchSolutionViaPac(r.value);
|
|
168
|
-
if (details) {
|
|
169
|
-
const name = details.friendlyname || details.uniquename || r.value;
|
|
170
|
-
r.label = `${name} (${details.prefix || '?'}_)`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return results.filter((r) => r.label && !r.label.includes('Default Solution') && !r.label.includes('Common Data'));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Parse utilities ─────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
function isGuid(s) {
|
|
181
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function extractGuid(text, hint) {
|
|
185
|
-
const guids = [...text.matchAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi)]
|
|
186
|
-
.map((m) => m[0].toLowerCase());
|
|
187
|
-
if (hint) return guids.find((g) => g === hint.toLowerCase()) || guids[0] || '';
|
|
188
|
-
return guids[0] || '';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function extractPattern(text, re) {
|
|
192
|
-
const m = text.match(re);
|
|
193
|
-
return m ? m[1] : '';
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Extract data tokens from PAC CLI output (skip header/banner lines). */
|
|
197
|
-
function extractDataTokens(output) {
|
|
198
|
-
const lines = output.split(/\r?\n/).filter((l) =>
|
|
199
|
-
l.trim() &&
|
|
200
|
-
!l.startsWith('Connected') &&
|
|
201
|
-
!l.startsWith('Microsoft') &&
|
|
202
|
-
!l.startsWith('Version:') &&
|
|
203
|
-
!l.startsWith('Online') &&
|
|
204
|
-
!l.startsWith('Feedback'),
|
|
205
|
-
);
|
|
206
|
-
const headerKeywords = ['uniquename', 'friendlyname', 'solutionid', 'publisherid', 'customizationprefix', 'version'];
|
|
207
|
-
const dataLines = lines.filter((l) => !headerKeywords.some((kw) => l.toLowerCase().includes(kw)));
|
|
208
|
-
return dataLines.join(' ').split(/\s+/).filter(Boolean);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Extract the friendly name from PAC output by removing known tokens. */
|
|
212
|
-
function extractFriendlyName(output, knownTokens) {
|
|
213
|
-
const lines = output.split(/\r?\n/).filter((l) =>
|
|
214
|
-
l.trim() &&
|
|
215
|
-
!l.startsWith('Connected') &&
|
|
216
|
-
!l.startsWith('Microsoft') &&
|
|
217
|
-
!l.startsWith('Version:') &&
|
|
218
|
-
!l.startsWith('Online') &&
|
|
219
|
-
!l.startsWith('Feedback'),
|
|
220
|
-
);
|
|
221
|
-
const headerKeywords = ['uniquename', 'friendlyname', 'solutionid', 'publisherid', 'customizationprefix', 'version', 'customizationoptionvalueprefix'];
|
|
222
|
-
const dataLines = lines.filter((l) => !headerKeywords.some((kw) => l.toLowerCase().includes(kw)));
|
|
223
|
-
let text = dataLines.join(' ');
|
|
224
|
-
// Remove known tokens (GUIDs, version, uniquename, prefix)
|
|
225
|
-
for (const token of knownTokens) {
|
|
226
|
-
if (token) text = text.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), '');
|
|
227
|
-
}
|
|
228
|
-
// Clean up multiple spaces and trim
|
|
229
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
209
|
+
const systemNames = ['default', 'common data services default solution'];
|
|
210
|
+
|
|
211
|
+
return rows
|
|
212
|
+
.filter((r) => {
|
|
213
|
+
const id = (r['solutionid'] || '').toLowerCase();
|
|
214
|
+
const name = (r['friendlyname'] || '').toLowerCase();
|
|
215
|
+
return id && !systemGuids.has(id) && !systemNames.includes(name);
|
|
216
|
+
})
|
|
217
|
+
.map((r) => {
|
|
218
|
+
const name = r['friendlyname'] || r['uniquename'] || r['solutionid'];
|
|
219
|
+
const prefix = r['pub.customizationprefix'] || '?';
|
|
220
|
+
return { value: r['solutionid'].toLowerCase(), label: `${name} (${prefix}_)` };
|
|
221
|
+
});
|
|
230
222
|
}
|
|
231
223
|
|
|
232
224
|
// ─── Dataverse API helpers (SPN) ─────────────────────────────────────
|
|
@@ -350,7 +342,7 @@ export default {
|
|
|
350
342
|
'3. Click on the solution to open it',
|
|
351
343
|
'4. Copy the URL from your browser\'s address bar',
|
|
352
344
|
'5. Paste it here — it looks like:',
|
|
353
|
-
' https://make.powerapps.com/
|
|
345
|
+
' https://make.powerapps.com/environments/.../solutions/{guid}',
|
|
354
346
|
].join('\n'),
|
|
355
347
|
});
|
|
356
348
|
|
|
@@ -450,7 +442,8 @@ export default {
|
|
|
450
442
|
|
|
451
443
|
// ── Paste URL → extract GUID → fetch via PAC ──
|
|
452
444
|
if (selection === PASTE_URL) {
|
|
453
|
-
|
|
445
|
+
// Strip trailing punctuation that users may accidentally include when pasting from a document.
|
|
446
|
+
const url = String(answers.SOLUTION_URL || '').trim().replace(/[.,;:!?]+$/, '');
|
|
454
447
|
const solutionId = extractSolutionIdFromUrl(url);
|
|
455
448
|
if (!solutionId) throw new Error('Could not find a solution GUID in that URL. Paste the full Maker Portal solution URL (…/solutions/{guid}).');
|
|
456
449
|
|
|
@@ -23,8 +23,8 @@ function runCommand(log, command, opts = {}) {
|
|
|
23
23
|
return new Promise((resolvePromise) => {
|
|
24
24
|
log.info(`$ ${command}`);
|
|
25
25
|
const child = process.platform === 'win32'
|
|
26
|
-
? spawn(process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', command], { cwd: opts.cwd ||
|
|
27
|
-
: spawn('sh', ['-c', command], { cwd: opts.cwd ||
|
|
26
|
+
? spawn(process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', command], { cwd: opts.cwd || process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] })
|
|
27
|
+
: spawn('sh', ['-c', command], { cwd: opts.cwd || process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
28
28
|
child.stdout.setEncoding('utf-8');
|
|
29
29
|
child.stderr.setEncoding('utf-8');
|
|
30
30
|
child.stdout.on('data', (chunk) => log.info(String(chunk).trimEnd()));
|
|
@@ -40,7 +40,7 @@ function runCommand(log, command, opts = {}) {
|
|
|
40
40
|
function runFile(log, file, args, opts = {}) {
|
|
41
41
|
return new Promise((resolvePromise) => {
|
|
42
42
|
log.info(`$ ${SHELL.formatCommandForLog(file, args)}`);
|
|
43
|
-
const child = SHELL.spawnSafe(file, args, { cwd: opts.cwd ||
|
|
43
|
+
const child = SHELL.spawnSafe(file, args, { cwd: opts.cwd || process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
44
44
|
child.stdout.setEncoding('utf-8');
|
|
45
45
|
child.stderr.setEncoding('utf-8');
|
|
46
46
|
child.stdout.on('data', (chunk) => log.info(String(chunk).trimEnd()));
|
|
@@ -68,7 +68,7 @@ function hasCommand(name) {
|
|
|
68
68
|
|
|
69
69
|
function resolveCredentialValues(state) {
|
|
70
70
|
return PAC_TARGET.resolveCredentialValues({
|
|
71
|
-
rootDir:
|
|
71
|
+
rootDir: state.PROJECT_DIR || process.cwd(),
|
|
72
72
|
opBin: process.env.OP_BIN || (hasCommand('op') ? 'op' : null),
|
|
73
73
|
source: state.AUTH_MODE || 'auto',
|
|
74
74
|
});
|
|
@@ -77,7 +77,7 @@ function resolveCredentialValues(state) {
|
|
|
77
77
|
function verifyPacTarget({ pac, projectDir, state, credentialValues, profileType, requirePowerConfig, requirePowerConfigTarget }) {
|
|
78
78
|
return PAC_TARGET.selectAndVerifyPacProfile({
|
|
79
79
|
pac,
|
|
80
|
-
rootDir:
|
|
80
|
+
rootDir: projectDir || process.cwd(),
|
|
81
81
|
wizardState: {
|
|
82
82
|
WIZARD_TARGET_ENV: state.WIZARD_TARGET_ENV || 'dev',
|
|
83
83
|
PP_ENV_DEV: state.PP_ENV_DEV || '',
|
|
@@ -148,7 +148,7 @@ export default {
|
|
|
148
148
|
},
|
|
149
149
|
|
|
150
150
|
questions(state) {
|
|
151
|
-
const rootDefault = state.PROJECT_DIR ||
|
|
151
|
+
const rootDefault = state.PROJECT_DIR || process.cwd();
|
|
152
152
|
const existingOrigin = SHELL.run('git remote get-url origin', { cwd: rootDefault }) || '';
|
|
153
153
|
const needsRemote = !existingOrigin || /PAppsCAFoundations/i.test(existingOrigin);
|
|
154
154
|
return [
|
|
@@ -194,7 +194,7 @@ export default {
|
|
|
194
194
|
|
|
195
195
|
async apply(answers, state, log) {
|
|
196
196
|
const appName = state.APP_NAME || 'Power Apps Code App';
|
|
197
|
-
const projectDir = resolve(String(answers.PROJECT_DIR ||
|
|
197
|
+
const projectDir = resolve(String(answers.PROJECT_DIR || process.cwd()).trim());
|
|
198
198
|
const foundationLogger = makeFoundationLogger(log);
|
|
199
199
|
|
|
200
200
|
if (existsSync(projectDir) && readdirSync(projectDir).length > 0 && answers.CONTINUE_NONEMPTY !== true) {
|
|
@@ -208,7 +208,7 @@ export default {
|
|
|
208
208
|
log.info('Downloading starter template...');
|
|
209
209
|
const templateArgs = ['--yes', 'degit', 'microsoft/PowerAppsCodeApps/templates/starter', projectDir];
|
|
210
210
|
if (dirNotEmpty) templateArgs.push('--force');
|
|
211
|
-
const templateOk = await runFile(log, toolCommand('npx'), templateArgs, { cwd:
|
|
211
|
+
const templateOk = await runFile(log, toolCommand('npx'), templateArgs, { cwd: projectDir });
|
|
212
212
|
if (!templateOk) {
|
|
213
213
|
log.warn('Template download failed. Creating minimal project structure instead.');
|
|
214
214
|
SCAFFOLD.createMinimalProject(projectDir, appName);
|