feat(profiles): add opt-in Lean/Full and hybrid Auto selection

This commit is contained in:
haelyra
2026-09-27 16:46:07 -04:00
parent e482e57941
commit 0765f7a2ca
342 changed files with 18212 additions and 209 deletions
+43 -12
View File
@@ -78,6 +78,35 @@ function runSetup(fixture, args, options = {}) {
function quoteShellArgument(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
// Answer only after the PTY displays a prompt. Fixed-delay pipes can deliver
// blank defaults and EOF before the wizard creates its readline interface.
function driveInteractiveTerminal() {
const { spawn } = require('child_process');
const { pseudoTerminalCommand, answers } = JSON.parse(process.argv[1]);
// Node pipes are sockets on macOS; script requires a real pipe for stdin.
const child = spawn('sh', ['-c', `cat | ${pseudoTerminalCommand}`], { stdio: ['pipe', 'pipe', 'pipe'] });
let pending = '';
let answerIndex = 0;
child.stdout.on('data', chunk => {
process.stdout.write(chunk);
pending += chunk.toString('utf8');
const prompt = /Choose(?: \[\d+\])?: |\[y\/N\] /.exec(pending);
if (!prompt) return;
pending = pending.slice(prompt.index + prompt[0].length);
if (answerIndex >= answers.length) { child.stdin.end(); return; }
const answer = answers[answerIndex++];
child.stdin.write(answer === '\u0004' ? answer : `${answer}\n`);
if (answerIndex === answers.length) child.stdin.end();
});
child.stderr.on('data', chunk => process.stderr.write(chunk));
child.stdin.on('error', error => {
if (error.code !== 'EPIPE') { process.stderr.write(error.message); process.exitCode = 1; }
});
child.on('error', error => { process.stderr.write(error.message); process.exitCode = 1; });
child.on('close', code => { process.exitCode = code ?? 1; });
}
function runInteractiveEccSetup(fixture, options = {}) {
if (process.platform === 'win32') {
return null;
@@ -85,12 +114,18 @@ function runInteractiveEccSetup(fixture, options = {}) {
const args = options.args || ['--dry-run'];
const answers = options.answers || ['3', '3'];
const command = [
const setupCommand = [
process.execPath,
eccScript,
'setup',
...args,
];
const command = options.delayedStartup
? [process.execPath, '-e', `setTimeout(() => {
const result = require('child_process').spawnSync(process.argv[1], process.argv.slice(2), { stdio: 'inherit' });
process.exitCode = result.status ?? 1;
}, 1250);`, ...setupCommand]
: setupCommand;
const scriptArgs = process.platform === 'darwin'
? ['-q', '-e', '/dev/null', ...command]
: [
@@ -100,16 +135,11 @@ function runInteractiveEccSetup(fixture, options = {}) {
command.map(quoteShellArgument).join(' '),
'/dev/null',
];
const pseudoTerminalCommand = ['script', ...scriptArgs]
.map(quoteShellArgument)
.join(' ');
const answerCommands = answers
.map(answer => `sleep 0.5; printf '%s\\n' ${quoteShellArgument(answer)}`)
.join('; ');
return spawnSync('sh', [
'-c',
`(${answerCommands}; sleep 0.1) | ${pseudoTerminalCommand}`,
const pseudoTerminalCommand = ['script', ...scriptArgs].map(quoteShellArgument).join(' ');
return spawnSync(process.execPath, [
'-e',
`(${driveInteractiveTerminal.toString()})();`,
JSON.stringify({ pseudoTerminalCommand, answers }),
], {
cwd: fixture.projectRoot,
env: {
@@ -386,8 +416,8 @@ test('setup automatically migrates an existing install to the selected scope and
const calls = readCalls(fixture);
assert.ok(calls.some(argv => (
argv.join(' ') === 'plugin install ecc@ecc --scope user'
+ ' --config hooks_enabled=true --config hook_profile=minimal'
)));
assert.ok(calls.every(argv => !argv.includes('--config')));
assert.ok(calls.some(argv => (
argv.join(' ') === 'plugin uninstall ecc@ecc --scope local --keep-data'
)));
@@ -914,6 +944,7 @@ test('interactive defaults preserve an existing install scope and hook preferenc
const result = runInteractiveEccSetup(fixture, {
args: ['--dry-run'],
answers: ['', ''],
delayedStartup: true,
});
assert.ifError(result.error);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);