@pacaf/wizard-ux 2.0.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 +84 -0
- package/bin/pacaf-wizard-ux.mjs +20 -0
- package/dist/assets/index-BVelUveV.js +127 -0
- package/dist/index.html +36 -0
- package/index.html +36 -0
- package/package.json +67 -0
- package/scripts/fix-pty-perms.mjs +34 -0
- package/server/index.mjs +144 -0
- package/server/lib/dataverse-bridge.mjs +51 -0
- package/server/lib/process-runner.mjs +117 -0
- package/server/lib/state-bridge.mjs +40 -0
- package/server/routes/onepassword.mjs +16 -0
- package/server/routes/pty.mjs +124 -0
- package/server/routes/state.mjs +88 -0
- package/server/routes/steps.mjs +62 -0
- package/server/routes/stream.mjs +49 -0
- package/server/routes/system.mjs +43 -0
- package/server/steps/01-prerequisites.mjs +127 -0
- package/server/steps/02-project-and-env.mjs +108 -0
- package/server/steps/03-app-registration.mjs +482 -0
- package/server/steps/04-auth-setup.mjs +435 -0
- package/server/steps/05-publisher.mjs +581 -0
- package/server/steps/06-solution.mjs +41 -0
- package/server/steps/07-scaffold.mjs +356 -0
- package/server/steps/08-connectors.mjs +438 -0
- package/server/steps/09-verify-deploy.mjs +212 -0
- package/server/steps/index.mjs +24 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Routes for /api/state
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { readState, writeState, resetStateFile, getCompletedStep } from '../lib/state-bridge.mjs';
|
|
5
|
+
import { TOTAL_STEPS } from '../steps/index.mjs';
|
|
6
|
+
|
|
7
|
+
function pickTargetEnvUrl(state) {
|
|
8
|
+
const target = String(state.WIZARD_TARGET_ENV || 'dev').toLowerCase();
|
|
9
|
+
const map = {
|
|
10
|
+
dev: state.PP_ENV_DEV,
|
|
11
|
+
test: state.PP_ENV_TEST,
|
|
12
|
+
prod: state.PP_ENV_PROD,
|
|
13
|
+
};
|
|
14
|
+
const raw = String(map[target] || state.PP_ENV_DEV || '').trim();
|
|
15
|
+
if (!raw) return { target, environmentUrl: '' };
|
|
16
|
+
return { target, environmentUrl: raw.replace(/\/$/, '') };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readPowerAppInfo(rootDir, state) {
|
|
20
|
+
const projectDir = resolve(rootDir, String(state.PROJECT_DIR || '.'));
|
|
21
|
+
const powerConfigPath = join(projectDir, 'power.config.json');
|
|
22
|
+
|
|
23
|
+
// The deployed Code App URL (apps.powerapps.com/play/...) is captured from
|
|
24
|
+
// `pac code push` stdout in Step 9 and persisted to wizard state as
|
|
25
|
+
// DEPLOYED_APP_URL. NOTE: power.config.json's `localAppUrl` is the *local*
|
|
26
|
+
// dev server URL (http://localhost:3000) — never use it for the launch CTA.
|
|
27
|
+
const deployedUrl = String(state.DEPLOYED_APP_URL || '').trim();
|
|
28
|
+
|
|
29
|
+
let appId = '';
|
|
30
|
+
if (existsSync(powerConfigPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(powerConfigPath, 'utf-8'));
|
|
33
|
+
appId = String(parsed?.appId || '').trim();
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore — appId is optional for the launch CTA
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!deployedUrl && !appId) return null;
|
|
40
|
+
|
|
41
|
+
const { target, environmentUrl } = pickTargetEnvUrl(state);
|
|
42
|
+
return {
|
|
43
|
+
appId,
|
|
44
|
+
targetEnv: target,
|
|
45
|
+
environmentUrl,
|
|
46
|
+
launchUrl: deployedUrl,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default async function stateRoutes(app, opts) {
|
|
51
|
+
const { rootDir } = opts;
|
|
52
|
+
|
|
53
|
+
app.get('/', async () => {
|
|
54
|
+
const state = readState(rootDir);
|
|
55
|
+
const completed = getCompletedStep(state);
|
|
56
|
+
const powerApp = readPowerAppInfo(rootDir, state);
|
|
57
|
+
return {
|
|
58
|
+
state,
|
|
59
|
+
completed,
|
|
60
|
+
next: Math.min(completed + 1, TOTAL_STEPS),
|
|
61
|
+
totalSteps: TOTAL_STEPS,
|
|
62
|
+
powerApp,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
app.put('/', async (req, reply) => {
|
|
67
|
+
const partial = req.body || {};
|
|
68
|
+
if (typeof partial !== 'object' || Array.isArray(partial)) {
|
|
69
|
+
return reply.code(400).send({ error: 'Body must be an object' });
|
|
70
|
+
}
|
|
71
|
+
const merged = writeState(rootDir, partial);
|
|
72
|
+
return { state: merged };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
app.post('/reset', async () => {
|
|
76
|
+
resetStateFile(rootDir);
|
|
77
|
+
return { ok: true };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.post('/jump', async (req, reply) => {
|
|
81
|
+
const target = parseInt(req.body?.step, 10);
|
|
82
|
+
if (!target || target < 1 || target > TOTAL_STEPS) {
|
|
83
|
+
return reply.code(400).send({ error: `step must be 1..${TOTAL_STEPS}` });
|
|
84
|
+
}
|
|
85
|
+
const merged = writeState(rootDir, { COMPLETED_STEP: target - 1 });
|
|
86
|
+
return { state: merged };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Routes for /api/steps
|
|
2
|
+
import { STEPS, getStep, TOTAL_STEPS } from '../steps/index.mjs';
|
|
3
|
+
import { readState, writeState, getCompletedStep } from '../lib/state-bridge.mjs';
|
|
4
|
+
import { newRun, runInline } from '../lib/process-runner.mjs';
|
|
5
|
+
|
|
6
|
+
export default async function stepsRoutes(app, { rootDir }) {
|
|
7
|
+
// GET /api/steps — list with status
|
|
8
|
+
app.get('/', async () => {
|
|
9
|
+
const state = readState(rootDir);
|
|
10
|
+
const completed = getCompletedStep(state);
|
|
11
|
+
return {
|
|
12
|
+
totalSteps: TOTAL_STEPS,
|
|
13
|
+
completed,
|
|
14
|
+
steps: STEPS.map((s) => ({
|
|
15
|
+
...s.meta,
|
|
16
|
+
status: s.meta.number <= completed ? 'done' : s.meta.number === completed + 1 ? 'current' : 'pending',
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// GET /api/steps/:n/questions
|
|
22
|
+
app.get('/:n/questions', async (req, reply) => {
|
|
23
|
+
const n = parseInt(req.params.n, 10);
|
|
24
|
+
if (!n || n < 1 || n > TOTAL_STEPS) return reply.code(404).send({ error: 'Unknown step' });
|
|
25
|
+
const step = getStep(n);
|
|
26
|
+
const state = readState(rootDir);
|
|
27
|
+
const questions = await step.questions(state);
|
|
28
|
+
return {
|
|
29
|
+
meta: step.meta,
|
|
30
|
+
questions,
|
|
31
|
+
state, // include for clientside conditional rendering convenience
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// POST /api/steps/:n/apply
|
|
36
|
+
app.post('/:n/apply', async (req, reply) => {
|
|
37
|
+
const n = parseInt(req.params.n, 10);
|
|
38
|
+
if (!n || n < 1 || n > TOTAL_STEPS) return reply.code(404).send({ error: 'Unknown step' });
|
|
39
|
+
const step = getStep(n);
|
|
40
|
+
if (!step.meta.canRunInBrowser) {
|
|
41
|
+
return reply.code(409).send({ error: 'Step cannot run in WizardUX' });
|
|
42
|
+
}
|
|
43
|
+
const answers = req.body?.answers || {};
|
|
44
|
+
const run = newRun();
|
|
45
|
+
|
|
46
|
+
// Kick off the apply asynchronously; client subscribes to /stream for log
|
|
47
|
+
const state = readState(rootDir);
|
|
48
|
+
queueMicrotask(async () => {
|
|
49
|
+
const outcome = await runInline(run, async (log) => {
|
|
50
|
+
const result = await step.apply(answers, state, log);
|
|
51
|
+
if (result?.stateUpdate) writeState(rootDir, result.stateUpdate);
|
|
52
|
+
if (result?.completedStep != null) {
|
|
53
|
+
writeState(rootDir, { COMPLETED_STEP: Math.max(getCompletedStep(readState(rootDir)), result.completedStep) });
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
});
|
|
57
|
+
void outcome;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return { runId: run.id };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Routes for /api/steps/:n/stream — SSE log streaming
|
|
2
|
+
import { getRun } from '../lib/process-runner.mjs';
|
|
3
|
+
|
|
4
|
+
export default async function streamRoutes(app /* , opts */) {
|
|
5
|
+
app.get('/:n/stream', async (req, reply) => {
|
|
6
|
+
const runId = req.query?.runId;
|
|
7
|
+
if (!runId) return reply.code(400).send({ error: 'runId required' });
|
|
8
|
+
const run = getRun(runId);
|
|
9
|
+
if (!run) return reply.code(404).send({ error: 'Unknown runId (may have been GC\'d)' });
|
|
10
|
+
|
|
11
|
+
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
|
12
|
+
reply.raw.setHeader('Cache-Control', 'no-cache');
|
|
13
|
+
reply.raw.setHeader('Connection', 'keep-alive');
|
|
14
|
+
reply.raw.flushHeaders?.();
|
|
15
|
+
|
|
16
|
+
const send = (event, data) => {
|
|
17
|
+
reply.raw.write(`event: ${event}\n`);
|
|
18
|
+
reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Replay buffered lines to late joiners
|
|
22
|
+
for (const line of run.lines) send('line', line);
|
|
23
|
+
if (run.deviceCode) send('deviceCode', run.deviceCode);
|
|
24
|
+
if (run.status === 'done' || run.status === 'error') {
|
|
25
|
+
send('end', { status: run.status, exitCode: run.exitCode, error: run.error });
|
|
26
|
+
reply.raw.end();
|
|
27
|
+
return reply;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const onLine = (line) => send('line', line);
|
|
31
|
+
const onDeviceCode = (dc) => send('deviceCode', dc);
|
|
32
|
+
const onEnd = () => {
|
|
33
|
+
send('end', { status: run.status, exitCode: run.exitCode, error: run.error });
|
|
34
|
+
reply.raw.end();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
run.on('line', onLine);
|
|
38
|
+
run.on('deviceCode', onDeviceCode);
|
|
39
|
+
run.on('end', onEnd);
|
|
40
|
+
|
|
41
|
+
req.raw.on('close', () => {
|
|
42
|
+
run.off('line', onLine);
|
|
43
|
+
run.off('deviceCode', onDeviceCode);
|
|
44
|
+
run.off('end', onEnd);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return reply;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Routes for /api/system — environment & tooling diagnostics
|
|
2
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir, platform, release } from 'node:os';
|
|
6
|
+
import { pacPath as resolvePacPath, runSafe } from '../../../wizard/lib/shell.mjs';
|
|
7
|
+
|
|
8
|
+
function safeRun(cmd) {
|
|
9
|
+
try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); }
|
|
10
|
+
catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function which(name) {
|
|
14
|
+
try {
|
|
15
|
+
const cmd = platform() === 'win32' ? 'where' : 'which';
|
|
16
|
+
execFileSync(cmd, [name], { stdio: 'ignore' });
|
|
17
|
+
return true;
|
|
18
|
+
} catch { return false; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pacVersion() {
|
|
22
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
23
|
+
const dotnetTool = join(homedir(), '.dotnet', 'tools', `pac${ext}`);
|
|
24
|
+
const pacPath = existsSync(dotnetTool) ? dotnetTool : resolvePacPath();
|
|
25
|
+
if (!pacPath) return null;
|
|
26
|
+
const out = runSafe(pacPath, []);
|
|
27
|
+
const m = out?.match(/Version:\s*(\S+)/i);
|
|
28
|
+
return m ? m[1] : (out ? 'unknown' : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function systemRoutes(app, { rootDir }) {
|
|
32
|
+
app.get('/', async () => ({
|
|
33
|
+
os: { platform: platform(), release: release() },
|
|
34
|
+
node: process.version,
|
|
35
|
+
git: safeRun('git --version')?.replace('git version ', '') || null,
|
|
36
|
+
dotnet: safeRun('dotnet --version'),
|
|
37
|
+
pac: pacVersion(),
|
|
38
|
+
op: which('op'),
|
|
39
|
+
rootDir,
|
|
40
|
+
branch: safeRun(`git -C "${rootDir}" rev-parse --abbrev-ref HEAD`) || null,
|
|
41
|
+
repoIsClean: safeRun(`git -C "${rootDir}" status --porcelain`) === '',
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Step 1 — Prerequisites. Read-only checks, no questions.
|
|
2
|
+
import { platform } from 'node:os';
|
|
3
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
4
|
+
import { pacPath, runSafe } from '../../../wizard/lib/shell.mjs';
|
|
5
|
+
|
|
6
|
+
function hasCommand(name) {
|
|
7
|
+
try {
|
|
8
|
+
const cmd = platform() === 'win32' ? 'where' : 'which';
|
|
9
|
+
execFileSync(cmd, [name], { stdio: 'ignore' });
|
|
10
|
+
return true;
|
|
11
|
+
} catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function tryRun(cmd) {
|
|
15
|
+
try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); }
|
|
16
|
+
catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
meta: {
|
|
21
|
+
number: 1,
|
|
22
|
+
title: 'Prerequisites',
|
|
23
|
+
description: 'Verify Node, Git, .NET SDK, PAC CLI, Python 3, and optional tools are present.',
|
|
24
|
+
canRunInBrowser: true,
|
|
25
|
+
readOnly: true,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
questions() { return []; },
|
|
29
|
+
|
|
30
|
+
async apply(_answers, _state, log) {
|
|
31
|
+
const checks = [];
|
|
32
|
+
let allOk = true;
|
|
33
|
+
let hasOp = false;
|
|
34
|
+
|
|
35
|
+
// Node
|
|
36
|
+
if (hasCommand('node')) {
|
|
37
|
+
const ver = tryRun('node --version') || '';
|
|
38
|
+
const major = parseInt(ver.replace(/^v/, ''), 10);
|
|
39
|
+
const ok = major >= 20;
|
|
40
|
+
checks.push({ name: 'Node.js', ok, value: ver, hint: ok ? null : 'Version 20+ required (https://nodejs.org/)' });
|
|
41
|
+
if (ok) log.ok(`Node.js ${ver}`); else { log.fail(`Node.js ${ver} — version 20+ required`); allOk = false; }
|
|
42
|
+
} else {
|
|
43
|
+
checks.push({ name: 'Node.js', ok: false, value: null, hint: 'Not installed (https://nodejs.org/)' });
|
|
44
|
+
log.fail('Node.js — not found'); allOk = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Git
|
|
48
|
+
if (hasCommand('git')) {
|
|
49
|
+
const ver = tryRun('git --version')?.replace('git version ', '') || '';
|
|
50
|
+
checks.push({ name: 'Git', ok: true, value: ver, hint: null });
|
|
51
|
+
log.ok(`Git ${ver}`);
|
|
52
|
+
} else {
|
|
53
|
+
checks.push({ name: 'Git', ok: false, value: null, hint: 'Not installed (https://git-scm.com/)' });
|
|
54
|
+
log.fail('Git — not found'); allOk = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// .NET
|
|
58
|
+
if (hasCommand('dotnet')) {
|
|
59
|
+
const ver = tryRun('dotnet --version') || '';
|
|
60
|
+
checks.push({ name: '.NET SDK', ok: true, value: ver, hint: null });
|
|
61
|
+
log.ok(`.NET SDK ${ver}`);
|
|
62
|
+
} else {
|
|
63
|
+
checks.push({ name: '.NET SDK', ok: false, value: null, hint: 'Required for PAC CLI (https://dotnet.microsoft.com/download)' });
|
|
64
|
+
log.fail('.NET SDK — not found'); allOk = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PAC CLI
|
|
68
|
+
const pac = pacPath();
|
|
69
|
+
if (pac) {
|
|
70
|
+
const header = runSafe(pac, []) || '';
|
|
71
|
+
const m = header.match(/Version:\s*(\S+)/i);
|
|
72
|
+
const pacVer = m ? m[1] : 'unknown';
|
|
73
|
+
const bad = pacVer.includes('2.3.2');
|
|
74
|
+
checks.push({ name: 'PAC CLI', ok: !bad, value: pacVer, hint: bad ? 'Version 2.3.2 has a known bug — install 2.2.1 instead' : null });
|
|
75
|
+
if (bad) { log.fail(`PAC CLI ${pacVer} — known-bad version`); allOk = false; }
|
|
76
|
+
else log.ok(`PAC CLI ${pacVer}`);
|
|
77
|
+
} else {
|
|
78
|
+
checks.push({ name: 'PAC CLI', ok: false, value: null, hint: 'Run: dotnet tool install -g Microsoft.PowerApps.CLI.Tool' });
|
|
79
|
+
log.fail('PAC CLI — not found'); allOk = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1Password CLI (optional)
|
|
83
|
+
if (hasCommand('op')) {
|
|
84
|
+
hasOp = true;
|
|
85
|
+
checks.push({ name: '1Password CLI', ok: true, value: 'available', hint: null, optional: true });
|
|
86
|
+
log.ok('1Password CLI available');
|
|
87
|
+
} else {
|
|
88
|
+
checks.push({ name: '1Password CLI', ok: false, value: null, hint: 'Optional — speeds up secret handling', optional: true });
|
|
89
|
+
log.info('1Password CLI not found (optional)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Python 3 (used by Dataverse-skills plugin)
|
|
93
|
+
let pythonCmd = null;
|
|
94
|
+
if (hasCommand('python3')) pythonCmd = 'python3';
|
|
95
|
+
else if (hasCommand('python')) pythonCmd = 'python';
|
|
96
|
+
|
|
97
|
+
if (pythonCmd) {
|
|
98
|
+
const pyVer = tryRun(`${pythonCmd} --version`)?.replace('Python ', '') || '';
|
|
99
|
+
const pyMajor = parseInt(pyVer, 10);
|
|
100
|
+
if (pyMajor >= 3) {
|
|
101
|
+
checks.push({ name: 'Python', ok: true, value: pyVer, hint: null });
|
|
102
|
+
log.ok(`Python ${pyVer}`);
|
|
103
|
+
} else {
|
|
104
|
+
checks.push({ name: 'Python', ok: false, value: pyVer, hint: 'Python 3+ required for Dataverse-skills plugin (https://www.python.org/downloads/)' });
|
|
105
|
+
log.warn(`Python ${pyVer} — Python 3+ required for Dataverse-skills plugin`);
|
|
106
|
+
log.info(' → Install Python 3: https://www.python.org/downloads/');
|
|
107
|
+
log.info(' → Then re-run this step to verify');
|
|
108
|
+
pythonCmd = null;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
checks.push({ name: 'Python', ok: false, value: null, hint: 'Required for Dataverse-skills plugin (https://www.python.org/downloads/)' });
|
|
112
|
+
log.warn('Python 3 — not found (required for Dataverse-skills plugin)');
|
|
113
|
+
log.info(' → Install Python 3: https://www.python.org/downloads/');
|
|
114
|
+
log.info(' → Then re-run this step to verify');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Note: The Dataverse-skills plugin manages its own Python SDK installation
|
|
118
|
+
// via the dv-connect skill. No separate pip install needed here.
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
stateUpdate: { HAS_OP: hasOp, PYTHON_CMD: pythonCmd },
|
|
122
|
+
result: { allOk, checks },
|
|
123
|
+
// Mark step complete only if no required tool is missing
|
|
124
|
+
completedStep: allOk ? 1 : null,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Step 2 — Project name + environment URLs.
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const VALIDATE_PATH = resolve(__dirname, '..', '..', '..', 'wizard', 'lib', 'validate.mjs');
|
|
7
|
+
const { isValidDataverseUrl, normalizeDataverseUrl } = await import(pathToFileURL(VALIDATE_PATH).href);
|
|
8
|
+
|
|
9
|
+
const URL_HINT = 'Format: https://org-name.crm.dynamics.com (the wizard adds https:// for you if you paste it without).';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
number: 2,
|
|
14
|
+
title: 'Project & Environment',
|
|
15
|
+
description: 'Name your app and point the wizard at your Dev (and optionally Test/Prod) Power Platform environments.',
|
|
16
|
+
canRunInBrowser: true,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
questions(state) {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
id: 'APP_NAME',
|
|
23
|
+
type: 'text',
|
|
24
|
+
label: 'App name',
|
|
25
|
+
help: 'A human-readable display name. e.g. "Project Tracker", "My Brain".',
|
|
26
|
+
required: true,
|
|
27
|
+
defaultValue: state.APP_NAME || '',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'PP_ENV_DEV',
|
|
31
|
+
type: 'url',
|
|
32
|
+
label: 'Dev environment URL',
|
|
33
|
+
help: 'The URL of your Power Platform development environment. Paste it straight from PPAC — with or without https://. ' + URL_HINT,
|
|
34
|
+
why: [
|
|
35
|
+
"If you haven't created one yet:",
|
|
36
|
+
'1. Open https://admin.powerplatform.microsoft.com',
|
|
37
|
+
'2. Environments → + New',
|
|
38
|
+
'3. Type: Developer or Sandbox · toggle "Add Dataverse" YES',
|
|
39
|
+
'4. Save, wait for provisioning, then copy the Environment URL.',
|
|
40
|
+
].join('\n'),
|
|
41
|
+
required: true,
|
|
42
|
+
defaultValue: state.PP_ENV_DEV || '',
|
|
43
|
+
validatePattern: 'dataverseUrl',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'PP_ENV_TEST',
|
|
47
|
+
type: 'url',
|
|
48
|
+
label: 'Test environment URL',
|
|
49
|
+
help: 'Optional. Leave blank if you do not have a separate Test environment yet.',
|
|
50
|
+
required: false,
|
|
51
|
+
defaultValue: state.PP_ENV_TEST || '',
|
|
52
|
+
validatePattern: 'dataverseUrl',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'PP_ENV_PROD',
|
|
56
|
+
type: 'url',
|
|
57
|
+
label: 'Prod environment URL',
|
|
58
|
+
help: 'Optional. Leave blank if you do not have a Prod environment yet.',
|
|
59
|
+
required: false,
|
|
60
|
+
defaultValue: state.PP_ENV_PROD || '',
|
|
61
|
+
validatePattern: 'dataverseUrl',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async apply(answers, _state, log) {
|
|
67
|
+
const errors = {};
|
|
68
|
+
const norm = (s) => normalizeDataverseUrl(s);
|
|
69
|
+
|
|
70
|
+
const appName = (answers.APP_NAME || '').trim();
|
|
71
|
+
if (!appName) errors.APP_NAME = 'Required';
|
|
72
|
+
|
|
73
|
+
const dev = norm(answers.PP_ENV_DEV);
|
|
74
|
+
if (!dev) errors.PP_ENV_DEV = 'Required';
|
|
75
|
+
else if (!isValidDataverseUrl(dev) && !isValidDataverseUrl(dev + '/')) errors.PP_ENV_DEV = URL_HINT;
|
|
76
|
+
|
|
77
|
+
const test = norm(answers.PP_ENV_TEST);
|
|
78
|
+
if (test && !isValidDataverseUrl(test) && !isValidDataverseUrl(test + '/')) {
|
|
79
|
+
log.warn(`Test URL "${test}" doesn't look standard, saving anyway.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const prod = norm(answers.PP_ENV_PROD);
|
|
83
|
+
if (prod && !isValidDataverseUrl(prod) && !isValidDataverseUrl(prod + '/')) {
|
|
84
|
+
log.warn(`Prod URL "${prod}" doesn't look standard, saving anyway.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Object.keys(errors).length > 0) {
|
|
88
|
+
const msg = Object.entries(errors).map(([k, v]) => `${k}: ${v}`).join('; ');
|
|
89
|
+
throw new Error(`Validation failed — ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.ok(`App name: ${appName}`);
|
|
93
|
+
log.ok(`Dev env: ${dev}`);
|
|
94
|
+
if (test) log.ok(`Test env: ${test}`); else log.info('Test env: (skipped)');
|
|
95
|
+
if (prod) log.ok(`Prod env: ${prod}`); else log.info('Prod env: (skipped)');
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
stateUpdate: {
|
|
99
|
+
APP_NAME: appName,
|
|
100
|
+
PP_ENV_DEV: dev,
|
|
101
|
+
PP_ENV_TEST: test,
|
|
102
|
+
PP_ENV_PROD: prod,
|
|
103
|
+
WIZARD_TARGET_ENV: 'dev',
|
|
104
|
+
},
|
|
105
|
+
completedStep: 2,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|