@pacaf/wizard-ux 3.0.7 → 3.0.9
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-zSgX3W7E.js → index-DODkyiWs.js} +31 -31
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/index.mjs +19 -5
- package/server/lib/dataverse-bridge.mjs +8 -0
- package/server/lib/process-runner.mjs +15 -4
- package/server/routes/system.mjs +16 -11
- package/server/steps/01-prerequisites.mjs +39 -15
- package/server/steps/03-app-registration.mjs +10 -16
- package/server/steps/04-auth-setup.mjs +29 -3
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-DODkyiWs.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.9",
|
|
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.1.
|
|
41
|
+
"@pacaf/wizard": "3.1.2"
|
|
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 { detectCloudSync, cloudSyncWarning } from '../../wizard/lib/cloud-sync-detect.mjs';
|
|
17
18
|
|
|
18
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const UX_DIR = resolve(__dirname, '..');
|
|
@@ -49,11 +50,15 @@ await app.register(cors, {
|
|
|
49
50
|
|
|
50
51
|
// Expose the CSRF token to the UI on a single endpoint. UI stores it in memory and
|
|
51
52
|
// echoes it on mutating calls. Token rotates per server start.
|
|
52
|
-
app.get('/api/handshake', async () =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
app.get('/api/handshake', async () => {
|
|
54
|
+
const cloud = detectCloudSync(ROOT_DIR);
|
|
55
|
+
return {
|
|
56
|
+
csrfToken: CSRF_TOKEN,
|
|
57
|
+
rootDir: ROOT_DIR,
|
|
58
|
+
startedAt: new Date().toISOString(),
|
|
59
|
+
cloudSync: cloud ? { detected: true, provider: cloud.provider } : { detected: false },
|
|
60
|
+
};
|
|
61
|
+
});
|
|
57
62
|
|
|
58
63
|
// Guard mutating routes
|
|
59
64
|
app.addHook('onRequest', async (req, reply) => {
|
|
@@ -130,6 +135,15 @@ if (!IS_DEV && haveDist) {
|
|
|
130
135
|
|
|
131
136
|
await app.listen({ host: HOST, port: PORT });
|
|
132
137
|
|
|
138
|
+
// Cloud-sync warning surfaced in the server console at startup. The UI also
|
|
139
|
+
// reads /api/handshake -> cloudSync to render an in-browser MessageBar.
|
|
140
|
+
{
|
|
141
|
+
const cloud = detectCloudSync(ROOT_DIR);
|
|
142
|
+
if (cloud) {
|
|
143
|
+
console.log(cloudSyncWarning(cloud.provider, ROOT_DIR));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
const url = `http://${HOST}:${PORT}`;
|
|
134
148
|
console.log('');
|
|
135
149
|
console.log(' ╭─────────────────────────────────────────────╮');
|
|
@@ -49,3 +49,11 @@ export function hasUsableSecret() {
|
|
|
49
49
|
if (secretsMod.getSecret()) return true;
|
|
50
50
|
return Boolean(secretsMod.recoverSecret());
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function persistSecretToCache(value) {
|
|
54
|
+
return secretsMod.persistSecretToCache(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clearSecretCache() {
|
|
58
|
+
return secretsMod.clearSecretCache();
|
|
59
|
+
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
6
|
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const SCRUB = await import(pathToFileURL(resolve(__dirname, '..', '..', '..', 'wizard', 'lib', 'scrub.mjs')).href);
|
|
7
12
|
|
|
8
13
|
const runs = new Map(); // runId -> Run
|
|
9
14
|
|
|
@@ -21,13 +26,17 @@ class Run extends EventEmitter {
|
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
push(stream, text) {
|
|
24
|
-
|
|
29
|
+
// Defense-in-depth: scrub any secret-shaped values from text before it
|
|
30
|
+
// hits the SSE log buffer. The buffer is streamed to the browser and
|
|
31
|
+
// rendered in the UI; never let a real secret land there.
|
|
32
|
+
const safe = SCRUB.scrubSecrets(text);
|
|
33
|
+
const evt = { stream, text: safe, ts: Date.now() };
|
|
25
34
|
this.lines.push(evt);
|
|
26
35
|
if (this.lines.length > 1000) this.lines.shift();
|
|
27
36
|
this.emit('line', evt);
|
|
28
37
|
// Detect device code prompts from pac auth create
|
|
29
|
-
const codeMatch =
|
|
30
|
-
const urlMatch =
|
|
38
|
+
const codeMatch = safe.match(/enter the code\s+([A-Z0-9]{6,12})/i);
|
|
39
|
+
const urlMatch = safe.match(/(https:\/\/microsoft\.com\/devicelogin)/i);
|
|
31
40
|
if (codeMatch) {
|
|
32
41
|
this.deviceCode = { code: codeMatch[1], url: 'https://microsoft.com/devicelogin', ts: Date.now() };
|
|
33
42
|
this.emit('deviceCode', this.deviceCode);
|
|
@@ -61,7 +70,9 @@ export function spawnInRun(run, file, args, opts = {}) {
|
|
|
61
70
|
return new Promise((resolveP) => {
|
|
62
71
|
run.status = 'running';
|
|
63
72
|
run.startedAt = Date.now();
|
|
64
|
-
|
|
73
|
+
// Display args with sensitive flag values redacted (e.g. --clientSecret ****)
|
|
74
|
+
const displayArgs = SCRUB.scrubArgs(args);
|
|
75
|
+
run.push('stdout', `$ ${file} ${displayArgs.join(' ')}\n`);
|
|
65
76
|
const child = spawn(file, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
66
77
|
run.child = child;
|
|
67
78
|
child.stdout.setEncoding('utf-8');
|
package/server/routes/system.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir, platform, release } from 'node:os';
|
|
6
6
|
import { pacPath as resolvePacPath, runSafe } from '@pacaf/wizard/lib/shell.mjs';
|
|
7
|
+
import { detectCloudSync } from '@pacaf/wizard/lib/cloud-sync-detect.mjs';
|
|
7
8
|
|
|
8
9
|
function safeRun(cmd) {
|
|
9
10
|
try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); }
|
|
@@ -29,15 +30,19 @@ function pacVersion() {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export default async function systemRoutes(app, { rootDir }) {
|
|
32
|
-
app.get('/', async () =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
app.get('/', async () => {
|
|
34
|
+
const cloud = detectCloudSync(rootDir);
|
|
35
|
+
return {
|
|
36
|
+
os: { platform: platform(), release: release() },
|
|
37
|
+
node: process.version,
|
|
38
|
+
git: safeRun('git --version')?.replace('git version ', '') || null,
|
|
39
|
+
dotnet: safeRun('dotnet --version'),
|
|
40
|
+
pac: pacVersion(),
|
|
41
|
+
op: which('op'),
|
|
42
|
+
rootDir,
|
|
43
|
+
cloudSync: cloud ? { detected: true, provider: cloud.provider } : { detected: false },
|
|
44
|
+
branch: safeRun(`git -C "${rootDir}" rev-parse --abbrev-ref HEAD`) || null,
|
|
45
|
+
repoIsClean: safeRun(`git -C "${rootDir}" status --porcelain`) === '',
|
|
46
|
+
};
|
|
47
|
+
});
|
|
43
48
|
}
|
|
@@ -90,27 +90,51 @@ export default {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Python 3 (used by Dataverse-skills plugin)
|
|
93
|
+
// On Windows, `python3` may resolve to the Microsoft Store App Execution Alias stub
|
|
94
|
+
// (%LOCALAPPDATA%\Microsoft\WindowsApps\python3.exe) which exits non-zero and prints
|
|
95
|
+
// "Python was not found; run without arguments to install from the Microsoft Store..."
|
|
96
|
+
// We treat any command whose --version output doesn't start with "Python 3" as absent
|
|
97
|
+
// and fall through to the next candidate. Priority order:
|
|
98
|
+
// python3 → python → py -3 (py launcher is Windows-only)
|
|
99
|
+
function tryPythonCmd(cmd) {
|
|
100
|
+
const raw = tryRun(`${cmd} --version`) || '';
|
|
101
|
+
// Store stub returns empty (non-zero exit trapped by tryRun) or the "not found" message
|
|
102
|
+
if (!raw.startsWith('Python 3')) return null;
|
|
103
|
+
return raw.replace('Python ', '').trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
let pythonCmd = null;
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
let pythonVersion = null;
|
|
108
|
+
|
|
109
|
+
const candidates = ['python3', 'python'];
|
|
110
|
+
if (platform() === 'win32') candidates.push('py');
|
|
111
|
+
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if (!hasCommand(candidate)) continue;
|
|
114
|
+
const ver = candidate === 'py' ? tryRun('py -3 --version')?.replace('Python ', '').trim() || null
|
|
115
|
+
: tryPythonCmd(candidate);
|
|
116
|
+
if (ver) {
|
|
117
|
+
pythonCmd = candidate === 'py' ? 'py' : candidate;
|
|
118
|
+
pythonVersion = ver;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
96
122
|
|
|
97
123
|
if (pythonCmd) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
checks.push({ name: 'Python', ok: true, value: pythonVersion, hint: null });
|
|
125
|
+
log.ok(`Python ${pythonVersion}${pythonCmd === 'py' ? ' (via py launcher)' : ''}`);
|
|
126
|
+
} else {
|
|
127
|
+
const winHint = platform() === 'win32'
|
|
128
|
+
? ' On Windows, ensure "Add python.exe to PATH" was checked during install, or disable the Microsoft Store python3 alias in Settings → Apps → Advanced app settings → App execution aliases.'
|
|
129
|
+
: '';
|
|
130
|
+
checks.push({ name: 'Python', ok: false, value: null, hint: `Required for Dataverse-skills plugin (https://www.python.org/downloads/).${winHint}` });
|
|
131
|
+
log.warn('Python 3 — not found (required for Dataverse-skills plugin)');
|
|
132
|
+
if (platform() === 'win32') {
|
|
133
|
+
log.info(' → Install Python 3 from https://www.python.org/downloads/ — check "Add python.exe to PATH"');
|
|
134
|
+
log.info(' → Or disable the Store stub: Settings → Apps → Advanced app settings → App execution aliases → turn off python3.exe');
|
|
103
135
|
} 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
136
|
log.info(' → Install Python 3: https://www.python.org/downloads/');
|
|
107
|
-
log.info(' → Then re-run this step to verify');
|
|
108
|
-
pythonCmd = null;
|
|
109
137
|
}
|
|
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
138
|
log.info(' → Then re-run this step to verify');
|
|
115
139
|
}
|
|
116
140
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Step 3 - Authentication. Browser-native auth method selection with optional 1Password sync.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, resolve, join } from 'node:path';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
5
4
|
import { platform } from 'node:os';
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
-
import { setSecret, hasUsableSecret } from '../lib/dataverse-bridge.mjs';
|
|
6
|
+
import { setSecret, hasUsableSecret, persistSecretToCache } from '../lib/dataverse-bridge.mjs';
|
|
8
7
|
|
|
9
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
9
|
// PACKAGE_DIR locates sibling @pacaf/wizard lib files (must stay __dirname-relative).
|
|
@@ -429,21 +428,16 @@ export default {
|
|
|
429
428
|
|
|
430
429
|
setSecret(clientSecret);
|
|
431
430
|
|
|
432
|
-
// Persist encrypted secret to
|
|
431
|
+
// Persist the encrypted secret to a per-machine OS-temp cache (NOT .env.local)
|
|
432
|
+
// so it survives wizard server restarts without ever placing the value
|
|
433
|
+
// inside the project folder. The project folder may be cloud-synced
|
|
434
|
+
// (OneDrive/Dropbox/iCloud), and content scanners pattern-match
|
|
435
|
+
// "PP_CLIENT_SECRET=" lines regardless of whether the value is encrypted.
|
|
433
436
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
let envContent = existsSync(envLocalPath) ? readFileSync(envLocalPath, 'utf-8') : '';
|
|
437
|
-
const encrypted = CRYPTO.encrypt(clientSecret);
|
|
438
|
-
if (envContent.match(/^PP_CLIENT_SECRET=.*$/m)) {
|
|
439
|
-
envContent = envContent.replace(/^PP_CLIENT_SECRET=.*$/m, `PP_CLIENT_SECRET=${encrypted}`);
|
|
440
|
-
} else {
|
|
441
|
-
envContent += `${envContent.endsWith('\n') || !envContent ? '' : '\n'}PP_CLIENT_SECRET=${encrypted}\n`;
|
|
442
|
-
}
|
|
443
|
-
writeFileSync(envLocalPath, envContent, 'utf-8');
|
|
444
|
-
log.ok('Secret encrypted and saved to .env.local');
|
|
437
|
+
persistSecretToCache(clientSecret);
|
|
438
|
+
log.ok('Secret cached securely outside the project folder');
|
|
445
439
|
} catch (err) {
|
|
446
|
-
log.warn(`Could not
|
|
440
|
+
log.warn(`Could not cache secret across restarts: ${err.message}`);
|
|
447
441
|
}
|
|
448
442
|
|
|
449
443
|
log.ok('Credential values captured');
|
|
@@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
-
import { getSecret, hasUsableSecret, recoverSecret, setSecret } from '../lib/dataverse-bridge.mjs';
|
|
7
|
+
import { getSecret, hasUsableSecret, recoverSecret, setSecret, persistSecretToCache, clearSecretCache } from '../lib/dataverse-bridge.mjs';
|
|
8
8
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
// PACKAGE_DIR locates sibling @pacaf/wizard lib files (must stay __dirname-relative).
|
|
@@ -14,6 +14,7 @@ const PROJECT_DIR = process.cwd();
|
|
|
14
14
|
const SHELL = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'shell.mjs')).href);
|
|
15
15
|
const CRYPTO = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'crypto.mjs')).href);
|
|
16
16
|
const PAC_TARGET = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'pac-target.mjs')).href);
|
|
17
|
+
const SCRUB = await import(pathToFileURL(resolve(PACKAGE_DIR, 'wizard', 'lib', 'scrub.mjs')).href);
|
|
17
18
|
|
|
18
19
|
function hasCommand(name) {
|
|
19
20
|
try {
|
|
@@ -24,6 +25,20 @@ function hasCommand(name) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
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
|
+
|
|
27
42
|
function normalizeUrl(value) {
|
|
28
43
|
return String(value || '').trim().replace(/\/+$/, '').toLowerCase();
|
|
29
44
|
}
|
|
@@ -34,7 +49,8 @@ function warnOnUrlDrift(log, key, stateUrl, credentialUrl) {
|
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
function formatPacAuthCreateError(profileName, url, output = '') {
|
|
37
|
-
const
|
|
52
|
+
const scrubbed = SCRUB.scrubSecrets(String(output || ''));
|
|
53
|
+
const lines = scrubbed.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
38
54
|
const detail = lines.find((line) => /^Error:/i.test(line)) || lines.at(-1) || 'pac auth create failed without a readable error message.';
|
|
39
55
|
return [
|
|
40
56
|
`${profileName} profile failed to create for ${url}.`,
|
|
@@ -93,7 +109,7 @@ function installPreCommitHook(log) {
|
|
|
93
109
|
|
|
94
110
|
function runLivePac(log, pac, args, opts = {}) {
|
|
95
111
|
return new Promise((resolvePromise) => {
|
|
96
|
-
log.info(`$ ${SHELL.formatCommandForLog(pac, args)}`);
|
|
112
|
+
log.info(`$ ${SCRUB.scrubSecrets(SHELL.formatCommandForLog(pac, args))}`);
|
|
97
113
|
const child = SHELL.spawnSafe(pac, args, { cwd: PROJECT_DIR, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
98
114
|
let settled = false;
|
|
99
115
|
const timeout = opts.timeoutMs
|
|
@@ -260,6 +276,10 @@ export default {
|
|
|
260
276
|
if (authMode === '1password' && state.OP_VAULT && state.OP_ITEM) {
|
|
261
277
|
const vault = state.OP_VAULT;
|
|
262
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 */ }
|
|
263
283
|
const envContent = [
|
|
264
284
|
'# .env - Safe to commit. Contains 1Password references, not secrets.',
|
|
265
285
|
`# Generated by setup wizard on ${dateStr}`,
|
|
@@ -326,6 +346,9 @@ export default {
|
|
|
326
346
|
const itemName = String(answers.OP_ITEM || state.OP_ITEM || '').trim();
|
|
327
347
|
if (!hasCommand('op')) throw new Error('1Password storage was selected, but op CLI is not available.');
|
|
328
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 */ }
|
|
329
352
|
const envContent = [
|
|
330
353
|
'# .env - Safe to commit. Contains 1Password references, not secrets.',
|
|
331
354
|
`# Generated by setup wizard on ${dateStr}`,
|
|
@@ -364,6 +387,9 @@ export default {
|
|
|
364
387
|
if (platform() !== 'win32') {
|
|
365
388
|
try { chmodSync(join(PROJECT_DIR, '.env.local'), 0o600); } catch { /* best effort */ }
|
|
366
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 */ }
|
|
367
393
|
log.ok('Wrote .env.local');
|
|
368
394
|
}
|
|
369
395
|
|