@pacaf/wizard-ux 3.0.12 → 3.1.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-C-wYJNnd.js +141 -0
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/index.mjs +2 -0
- package/server/lib/config-seed-allowlist.mjs +125 -0
- package/server/routes/config-seed.mjs +94 -0
- package/server/routes/state.mjs +10 -1
- package/server/steps/07-scaffold.mjs +66 -7
- package/server/steps/09-verify-deploy.mjs +13 -1
- package/dist/assets/index-BIqBKzCb.js +0 -127
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="/assets/index-
|
|
31
|
+
<script type="module" crossorigin src="/assets/index-C-wYJNnd.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.1.0",
|
|
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.
|
|
41
|
+
"@pacaf/wizard": "3.2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.0",
|
package/server/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import stepsRoutes from './routes/steps.mjs';
|
|
|
14
14
|
import streamRoutes from './routes/stream.mjs';
|
|
15
15
|
import ptyRoutes from './routes/pty.mjs';
|
|
16
16
|
import onepasswordRoutes from './routes/onepassword.mjs';
|
|
17
|
+
import configSeedRoutes from './routes/config-seed.mjs';
|
|
17
18
|
import { detectCloudSync, cloudSyncWarning } from '../../wizard/lib/cloud-sync-detect.mjs';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -84,6 +85,7 @@ await app.register(stepsRoutes, { prefix: '/api/steps', rootDir: ROOT_DIR });
|
|
|
84
85
|
await app.register(streamRoutes, { prefix: '/api/steps', rootDir: ROOT_DIR });
|
|
85
86
|
await app.register(ptyRoutes, { rootDir: ROOT_DIR, csrfToken: CSRF_TOKEN });
|
|
86
87
|
await app.register(onepasswordRoutes, { prefix: '/api/1password' });
|
|
88
|
+
await app.register(configSeedRoutes, { prefix: '/api/config-seed', rootDir: ROOT_DIR });
|
|
87
89
|
|
|
88
90
|
// Serve the UI — prebuilt dist/ by default; vite middleware only in dev mode
|
|
89
91
|
const distDir = join(UX_DIR, 'dist');
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// wizard-ux/server/lib/config-seed-allowlist.mjs
|
|
2
|
+
// Single source of truth for which wizard-state keys are portable across projects.
|
|
3
|
+
// Anything not in this list is excluded from export and rejected on import.
|
|
4
|
+
// New state keys added in future steps are excluded by default — opt in explicitly.
|
|
5
|
+
|
|
6
|
+
export const SEED_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
/** @typedef {'string'|'string[]'|'stringMap'} SeedType */
|
|
9
|
+
/** @typedef {{ key: string, type: SeedType, label: string }} SeedKeySpec */
|
|
10
|
+
|
|
11
|
+
/** @type {SeedKeySpec[]} */
|
|
12
|
+
export const CONFIG_SEED_KEYS = [
|
|
13
|
+
// Publisher (org-level)
|
|
14
|
+
{ key: 'PUBLISHER_PREFIX', type: 'string', label: 'Publisher prefix' },
|
|
15
|
+
{ key: 'PUBLISHER_ID', type: 'string', label: 'Publisher ID' },
|
|
16
|
+
{ key: 'PUBLISHER_NAME', type: 'string', label: 'Publisher name' },
|
|
17
|
+
{ key: 'PUBLISHER_DISPLAY_NAME', type: 'string', label: 'Publisher display name' },
|
|
18
|
+
{ key: 'CHOICE_VALUE_PREFIX', type: 'string', label: 'Choice value prefix' },
|
|
19
|
+
|
|
20
|
+
// Environments
|
|
21
|
+
{ key: 'PP_ENV_DEV', type: 'string', label: 'Dev environment URL' },
|
|
22
|
+
{ key: 'PP_ENV_TEST', type: 'string', label: 'Test environment URL' },
|
|
23
|
+
{ key: 'PP_ENV_PROD', type: 'string', label: 'Prod environment URL' },
|
|
24
|
+
|
|
25
|
+
// Auth profile references (NOT the secrets themselves)
|
|
26
|
+
{ key: 'AUTH_PROFILE_TYPE', type: 'string', label: 'Auth profile type' },
|
|
27
|
+
{ key: 'OP_VAULT', type: 'string', label: '1Password vault' },
|
|
28
|
+
{ key: 'OP_ITEM', type: 'string', label: '1Password item' },
|
|
29
|
+
|
|
30
|
+
// Agent preference
|
|
31
|
+
{ key: 'CODING_AGENT', type: 'string', label: 'Coding agent' },
|
|
32
|
+
|
|
33
|
+
// Connectors
|
|
34
|
+
{ key: 'CONNECTOR_API_IDS', type: 'string[]', label: 'Selected connector apiIds' },
|
|
35
|
+
{ key: 'CONNECTOR_CONNECTION_IDS', type: 'stringMap', label: 'Connection IDs by apiId' },
|
|
36
|
+
{ key: 'CUSTOM_CONNECTORS', type: 'string[]', label: 'Custom connector entries' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const KEY_BY_NAME = new Map(CONFIG_SEED_KEYS.map((s) => [s.key, s]));
|
|
40
|
+
|
|
41
|
+
// Second-line defense: even if someone adds a key to the allow-list by mistake,
|
|
42
|
+
// reject any key name that looks like a secret.
|
|
43
|
+
const SECRET_KEY_PATTERN = /secret|token|password|client_id|client_secret|private_key|api[_-]?key/i;
|
|
44
|
+
|
|
45
|
+
/** Filter a state object down to the allow-listed keys. Drops empty / undefined values. */
|
|
46
|
+
export function filterToSeed(state) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const spec of CONFIG_SEED_KEYS) {
|
|
49
|
+
const raw = state?.[spec.key];
|
|
50
|
+
if (raw === undefined || raw === null || raw === '') continue;
|
|
51
|
+
if (spec.type === 'string[]' && Array.isArray(raw) && raw.length === 0) continue;
|
|
52
|
+
if (spec.type === 'stringMap' && typeof raw === 'object' && Object.keys(raw).length === 0) continue;
|
|
53
|
+
out[spec.key] = raw;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate an inbound payload (the `values` object from an uploaded seed file).
|
|
60
|
+
* Returns { applied: {key,value}[], skipped: {key,reason}[] }.
|
|
61
|
+
* Never throws on per-key issues — collects them as skips so the UI can show them.
|
|
62
|
+
*/
|
|
63
|
+
export function validateSeed(values) {
|
|
64
|
+
const applied = [];
|
|
65
|
+
const skipped = [];
|
|
66
|
+
|
|
67
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)) {
|
|
68
|
+
return { applied: [], skipped: [{ key: '<root>', reason: 'Payload must be a JSON object.' }] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of Object.entries(values)) {
|
|
72
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
73
|
+
skipped.push({ key, reason: 'Key name looks like a secret; refusing to import.' });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const spec = KEY_BY_NAME.get(key);
|
|
77
|
+
if (!spec) {
|
|
78
|
+
skipped.push({ key, reason: 'Key is not in the allow-list.' });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const checked = checkType(spec, value);
|
|
82
|
+
if (checked.ok) {
|
|
83
|
+
applied.push({ key, value: checked.value });
|
|
84
|
+
} else {
|
|
85
|
+
skipped.push({ key, reason: checked.reason });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { applied, skipped };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkType(spec, value) {
|
|
93
|
+
if (spec.type === 'string') {
|
|
94
|
+
if (typeof value !== 'string') return { ok: false, reason: `Expected string, got ${typeOf(value)}.` };
|
|
95
|
+
if (value.length > 2048) return { ok: false, reason: 'String value exceeds 2048 chars.' };
|
|
96
|
+
return { ok: true, value };
|
|
97
|
+
}
|
|
98
|
+
if (spec.type === 'string[]') {
|
|
99
|
+
if (!Array.isArray(value)) return { ok: false, reason: `Expected array of strings, got ${typeOf(value)}.` };
|
|
100
|
+
if (value.length > 256) return { ok: false, reason: 'Array exceeds 256 entries.' };
|
|
101
|
+
for (const item of value) {
|
|
102
|
+
if (typeof item !== 'string') return { ok: false, reason: 'Array contains non-string entry.' };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, value };
|
|
105
|
+
}
|
|
106
|
+
if (spec.type === 'stringMap') {
|
|
107
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
108
|
+
return { ok: false, reason: `Expected object of string-to-string, got ${typeOf(value)}.` };
|
|
109
|
+
}
|
|
110
|
+
const keys = Object.keys(value);
|
|
111
|
+
if (keys.length > 256) return { ok: false, reason: 'Object exceeds 256 entries.' };
|
|
112
|
+
for (const k of keys) {
|
|
113
|
+
if (SECRET_KEY_PATTERN.test(k)) return { ok: false, reason: `Inner key ${k} looks like a secret.` };
|
|
114
|
+
if (typeof value[k] !== 'string') return { ok: false, reason: `Value at ${k} is not a string.` };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, value };
|
|
117
|
+
}
|
|
118
|
+
return { ok: false, reason: `Unknown allow-list type: ${spec.type}` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function typeOf(v) {
|
|
122
|
+
if (Array.isArray(v)) return 'array';
|
|
123
|
+
if (v === null) return 'null';
|
|
124
|
+
return typeof v;
|
|
125
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Routes for /api/config-seed — export/import portable wizard configuration "seeds".
|
|
2
|
+
// A seed is an allow-listed subset of .wizard-state.json — never secrets.
|
|
3
|
+
import { readState, writeState } from '../lib/state-bridge.mjs';
|
|
4
|
+
import {
|
|
5
|
+
CONFIG_SEED_KEYS, SEED_VERSION, filterToSeed, validateSeed,
|
|
6
|
+
} from '../lib/config-seed-allowlist.mjs';
|
|
7
|
+
|
|
8
|
+
const MAX_IMPORT_BYTES = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
export default async function configSeedRoutes(app, opts) {
|
|
11
|
+
const { rootDir } = opts;
|
|
12
|
+
|
|
13
|
+
// Metadata about what is exportable. Lets the UI render a preview/legend.
|
|
14
|
+
app.get('/keys', async () => ({
|
|
15
|
+
version: SEED_VERSION,
|
|
16
|
+
keys: CONFIG_SEED_KEYS.map(({ key, type, label }) => ({ key, type, label })),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Download the current state, filtered to the allow-list.
|
|
20
|
+
app.get('/export', async (_req, reply) => {
|
|
21
|
+
const state = readState(rootDir);
|
|
22
|
+
const values = filterToSeed(state);
|
|
23
|
+
const payload = {
|
|
24
|
+
$schema: 'https://aka.ms/pacaf/wizard-config/v1',
|
|
25
|
+
version: SEED_VERSION,
|
|
26
|
+
exportedAt: new Date().toISOString(),
|
|
27
|
+
values,
|
|
28
|
+
};
|
|
29
|
+
const filename = `pacaf-wizard-config-${new Date().toISOString().slice(0, 10)}.json`;
|
|
30
|
+
reply
|
|
31
|
+
.header('Content-Type', 'application/json; charset=utf-8')
|
|
32
|
+
.header('Content-Disposition', `attachment; filename="${filename}"`)
|
|
33
|
+
.send(JSON.stringify(payload, null, 2) + '\n');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Import a seed file. Body = parsed seed JSON. Validates against allow-list and merges.
|
|
37
|
+
// Modes:
|
|
38
|
+
// merge (default) — only write keys whose current value is empty/missing
|
|
39
|
+
// replace-allowlisted — overwrite every allow-listed key the seed provides
|
|
40
|
+
app.post('/import', async (req, reply) => {
|
|
41
|
+
const raw = req.body;
|
|
42
|
+
const size = Buffer.byteLength(JSON.stringify(raw ?? {}), 'utf-8');
|
|
43
|
+
if (size > MAX_IMPORT_BYTES) {
|
|
44
|
+
return reply.code(413).send({ error: `Payload too large (${size} > ${MAX_IMPORT_BYTES} bytes).` });
|
|
45
|
+
}
|
|
46
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
47
|
+
return reply.code(400).send({ error: 'Body must be a JSON object.' });
|
|
48
|
+
}
|
|
49
|
+
const mode = String(raw.mode || 'merge');
|
|
50
|
+
if (mode !== 'merge' && mode !== 'replace-allowlisted') {
|
|
51
|
+
return reply.code(400).send({ error: `Unknown mode: ${mode}` });
|
|
52
|
+
}
|
|
53
|
+
const seed = raw.seed;
|
|
54
|
+
if (!seed || typeof seed !== 'object') {
|
|
55
|
+
return reply.code(400).send({ error: 'Body must include a `seed` object.' });
|
|
56
|
+
}
|
|
57
|
+
if (seed.version !== undefined && Number(seed.version) > SEED_VERSION) {
|
|
58
|
+
return reply.code(400).send({
|
|
59
|
+
error: `Seed version ${seed.version} is newer than this wizard supports (max ${SEED_VERSION}). Upgrade the wizard.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const { applied, skipped } = validateSeed(seed.values || {});
|
|
63
|
+
|
|
64
|
+
const current = readState(rootDir);
|
|
65
|
+
const partial = {};
|
|
66
|
+
const written = [];
|
|
67
|
+
const preserved = [];
|
|
68
|
+
|
|
69
|
+
for (const { key, value } of applied) {
|
|
70
|
+
const existing = current[key];
|
|
71
|
+
const isEmpty = existing === undefined
|
|
72
|
+
|| existing === null
|
|
73
|
+
|| existing === ''
|
|
74
|
+
|| (Array.isArray(existing) && existing.length === 0)
|
|
75
|
+
|| (typeof existing === 'object' && !Array.isArray(existing) && Object.keys(existing).length === 0);
|
|
76
|
+
|
|
77
|
+
if (mode === 'merge' && !isEmpty) {
|
|
78
|
+
preserved.push({ key, reason: 'Existing value preserved (merge mode).' });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
partial[key] = value;
|
|
82
|
+
written.push({ key });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Object.keys(partial).length > 0) writeState(rootDir, partial);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
mode,
|
|
89
|
+
written,
|
|
90
|
+
preserved,
|
|
91
|
+
skipped,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
package/server/routes/state.mjs
CHANGED
|
@@ -16,6 +16,15 @@ function pickTargetEnvUrl(state) {
|
|
|
16
16
|
return { target, environmentUrl: raw.replace(/\/$/, '') };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Append ?hideNavBar=true (or & form) so the Power Apps "purple bar" is hidden
|
|
20
|
+
// by default for every Code App built from this template. See issue #44 and
|
|
21
|
+
// 04-deployment.instructions.md ("Default play URL: ?hideNavBar=true").
|
|
22
|
+
function withHideNavBar(url) {
|
|
23
|
+
if (!url) return url;
|
|
24
|
+
if (/[?&]hideNavBar=/i.test(url)) return url;
|
|
25
|
+
return url + (url.includes('?') ? '&' : '?') + 'hideNavBar=true';
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function readPowerAppInfo(rootDir, state) {
|
|
20
29
|
const projectDir = resolve(rootDir, String(state.PROJECT_DIR || '.'));
|
|
21
30
|
const powerConfigPath = join(projectDir, 'power.config.json');
|
|
@@ -43,7 +52,7 @@ function readPowerAppInfo(rootDir, state) {
|
|
|
43
52
|
appId,
|
|
44
53
|
targetEnv: target,
|
|
45
54
|
environmentUrl,
|
|
46
|
-
launchUrl: deployedUrl,
|
|
55
|
+
launchUrl: withHideNavBar(deployedUrl),
|
|
47
56
|
};
|
|
48
57
|
}
|
|
49
58
|
|
|
@@ -40,7 +40,9 @@ 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
|
|
43
|
+
const spawnOpts = { cwd: opts.cwd || process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] };
|
|
44
|
+
if (opts.env) spawnOpts.env = opts.env;
|
|
45
|
+
const child = SHELL.spawnSafe(file, args, spawnOpts);
|
|
44
46
|
child.stdout.setEncoding('utf-8');
|
|
45
47
|
child.stderr.setEncoding('utf-8');
|
|
46
48
|
child.stdout.on('data', (chunk) => log.info(String(chunk).trimEnd()));
|
|
@@ -53,6 +55,47 @@ function runFile(log, file, args, opts = {}) {
|
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// Build a noisy-but-reassuring env for `npm install` / `pnpm install` so the
|
|
59
|
+
// SSE-piped output stops looking frozen during long cold installs:
|
|
60
|
+
// - `npm_config_loglevel=http` is added at the CLI level (so it shows the
|
|
61
|
+
// `npm http fetch ...` per-package line).
|
|
62
|
+
// - `npm_config_progress=true` keeps progress hints on (npm still suppresses
|
|
63
|
+
// its TTY bar but at least won't strip extra hints).
|
|
64
|
+
// - `FORCE_COLOR=0` keeps ANSI escapes out of the SSE stream.
|
|
65
|
+
// - `CI` is unset to avoid npm dropping output thinking it is in CI.
|
|
66
|
+
function installEnv() {
|
|
67
|
+
const env = { ...process.env, npm_config_progress: 'true', FORCE_COLOR: '0' };
|
|
68
|
+
delete env.CI;
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Detect whether `pnpm` is available on PATH. pnpm prints staged progress
|
|
73
|
+
// (`Progress: resolved X, reused Y, downloaded Z`) even in non-TTY mode and
|
|
74
|
+
// is materially faster on a cold cache, so prefer it when present.
|
|
75
|
+
function detectPnpm() {
|
|
76
|
+
try {
|
|
77
|
+
execFileSync(process.platform === 'win32' ? 'where' : 'which', ['pnpm'], { stdio: 'ignore' });
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runInstall(log, { stage, label, projectDir, pnpm, mode, packages = [] }) {
|
|
85
|
+
log.info('');
|
|
86
|
+
log.info(`[${stage}] ${label} (typically 30s–3min on a cold cache)…`);
|
|
87
|
+
const bin = pnpm
|
|
88
|
+
? toolCommand('pnpm')
|
|
89
|
+
: toolCommand('npm');
|
|
90
|
+
const baseArgs = pnpm
|
|
91
|
+
? (mode === 'base' ? ['install'] : mode === 'dev' ? ['add', '-D', ...packages] : ['add', ...packages])
|
|
92
|
+
: (mode === 'base' ? ['install'] : mode === 'dev' ? ['install', '-D', ...packages] : ['install', ...packages]);
|
|
93
|
+
const noisyArgs = pnpm
|
|
94
|
+
? ['--reporter=append-only', ...baseArgs]
|
|
95
|
+
: ['--loglevel=http', '--no-audit', '--no-fund', ...baseArgs];
|
|
96
|
+
return runFile(log, bin, noisyArgs, { cwd: projectDir, env: installEnv() });
|
|
97
|
+
}
|
|
98
|
+
|
|
56
99
|
function toolCommand(name) {
|
|
57
100
|
return process.platform === 'win32' ? `${name}.cmd` : name;
|
|
58
101
|
}
|
|
@@ -233,16 +276,32 @@ export default {
|
|
|
233
276
|
}
|
|
234
277
|
|
|
235
278
|
log.info('Installing dependencies...');
|
|
236
|
-
|
|
237
|
-
|
|
279
|
+
const pnpm = detectPnpm();
|
|
280
|
+
if (pnpm) {
|
|
281
|
+
log.info('Detected pnpm — using it for faster installs and shared dependency cache.');
|
|
282
|
+
} else {
|
|
283
|
+
log.info('pnpm not found — using npm. Tip: `corepack enable && corepack prepare pnpm@latest --activate` makes Step 7 noticeably faster.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (await runInstall(log, { stage: '1/3', label: 'Installing base dependencies', projectDir, pnpm, mode: 'base' })) {
|
|
287
|
+
log.ok('[1/3] Base dependencies installed');
|
|
288
|
+
} else {
|
|
289
|
+
log.warn('[1/3] Base dependency install reported errors; continuing to merge required packages.');
|
|
290
|
+
}
|
|
238
291
|
|
|
239
292
|
const prodPkgs = SCAFFOLD.packageSpecs(SCAFFOLD.REQUIRED_RUNTIME_PACKAGES);
|
|
240
|
-
if (await
|
|
241
|
-
|
|
293
|
+
if (await runInstall(log, { stage: '2/3', label: 'Installing runtime packages (React, Fluent UI, TanStack Query, SDK)', projectDir, pnpm, mode: 'prod', packages: prodPkgs })) {
|
|
294
|
+
log.ok('[2/3] Runtime packages installed');
|
|
295
|
+
} else {
|
|
296
|
+
log.warn('[2/3] Some runtime packages failed to install.');
|
|
297
|
+
}
|
|
242
298
|
|
|
243
299
|
const devPkgs = SCAFFOLD.packageSpecs(SCAFFOLD.REQUIRED_DEV_PACKAGES);
|
|
244
|
-
if (await
|
|
245
|
-
|
|
300
|
+
if (await runInstall(log, { stage: '3/3', label: 'Installing dev dependencies (Vitest, ESLint, Playwright, @pacaf/scripts)', projectDir, pnpm, mode: 'dev', packages: devPkgs })) {
|
|
301
|
+
log.ok('[3/3] Dev packages installed');
|
|
302
|
+
} else {
|
|
303
|
+
log.warn('[3/3] Some dev packages failed to install.');
|
|
304
|
+
}
|
|
246
305
|
|
|
247
306
|
SCAFFOLD.writeConfig(projectDir, foundationLogger);
|
|
248
307
|
SCAFFOLD.mergePackageJsonScripts(projectDir, foundationLogger);
|
|
@@ -82,6 +82,16 @@ function runFileCapture(log, file, args, opts = {}) {
|
|
|
82
82
|
|
|
83
83
|
const PAC_HTTP_ERROR_RE = /HTTP error status:\s*[45]\d\d/i;
|
|
84
84
|
|
|
85
|
+
// Append ?hideNavBar=true (or &hideNavBar=true if the URL already has a query
|
|
86
|
+
// string) so the Power Apps "purple bar" — the top chrome rendered by the
|
|
87
|
+
// Power Apps host around any Code App — is hidden by default. See
|
|
88
|
+
// .github/instructions/04-deployment.instructions.md and issue #44.
|
|
89
|
+
function withHideNavBar(url) {
|
|
90
|
+
if (!url) return url;
|
|
91
|
+
if (/[?&]hideNavBar=/i.test(url)) return url;
|
|
92
|
+
return url + (url.includes('?') ? '&' : '?') + 'hideNavBar=true';
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
function resolveCredentialValues(state) {
|
|
86
96
|
return PAC_TARGET.resolveCredentialValues({
|
|
87
97
|
rootDir: PROJECT_DIR,
|
|
@@ -198,10 +208,12 @@ export default {
|
|
|
198
208
|
// "The app was successfully published. URL: https://apps.powerapps.com/play/e/<envId>/a/<appId>"
|
|
199
209
|
// Capture the first matching apps.powerapps.com/play URL and persist it
|
|
200
210
|
// to wizard state so the Summary can show the real launch URL.
|
|
211
|
+
// Always append ?hideNavBar=true so the deployed app hides the Power
|
|
212
|
+
// Apps "purple bar" by default (see issue #44 and 04-deployment.instructions.md).
|
|
201
213
|
const deployedUrlMatch = pushOutput.match(/https:\/\/apps\.powerapps\.com\/play\/[^\s'"<>)]+/i);
|
|
202
214
|
const stateUpdate = { PROJECT_DIR: projectDir };
|
|
203
215
|
if (deployedUrlMatch) {
|
|
204
|
-
const deployedUrl = deployedUrlMatch[0].replace(/[.,;]+$/, '');
|
|
216
|
+
const deployedUrl = withHideNavBar(deployedUrlMatch[0].replace(/[.,;]+$/, ''));
|
|
205
217
|
stateUpdate.DEPLOYED_APP_URL = deployedUrl;
|
|
206
218
|
log.ok(`App URL: ${deployedUrl}`);
|
|
207
219
|
} else {
|