修正 dev:all 摘要表输出时序

改用无引号纯文本表格显示启动服务和实际地址。

等待 Tauri Running DevCommand 后再打印摘要。

补充启动器输出转发、回归测试和开发运维说明。
This commit is contained in:
2026-10-01 19:03:52 +08:00
parent e9b3e2b531
commit c911a338f9
5 changed files with 113 additions and 11 deletions
+78 -8
View File
@@ -20,6 +20,7 @@ const rootReadyPollMs = 500;
const childGracefulTimeoutMs = 2_500;
const childForceTimeoutMs = 2_000;
const agcReadyPollMs = 500;
const tauriDevReadyMarker = '[ai-game-creator-shell] Tauri DevCommand 已启动';
const rootOnlyOptions = new Set([
'--database',
@@ -278,9 +279,39 @@ function buildStartupSummary(state, agcEndpoint) {
];
}
function printStartupSummary(state, agcEndpoint, table = console.table) {
console.log('\n[dev:all] 全部开发服务已启动');
table(buildStartupSummary(state, agcEndpoint));
function getDisplayWidth(value) {
return [...String(value)].reduce(
(width, character) => width + (character.charCodeAt(0) > 0xff ? 2 : 1),
0,
);
}
function renderStartupTable(rows) {
const columns = ['项目', '状态', '地址'];
const values = rows.map((row) => columns.map((column) => row[column] ?? '—'));
const widths = columns.map((column, index) =>
Math.max(
getDisplayWidth(column),
...values.map((row) => getDisplayWidth(row[index])),
),
);
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
const renderRow = (row) =>
`| ${row.map((value, index) => `${value}${' '.repeat(widths[index] - getDisplayWidth(value))}`).join(' | ')} |`;
return [
border,
renderRow(columns),
border,
...values.map(renderRow),
border,
].join('\n');
}
function printStartupSummary(state, agcEndpoint) {
console.log(
`\n[dev:all] 全部开发服务已启动\n${renderStartupTable(buildStartupSummary(state, agcEndpoint))}`,
);
}
function waitForChildExit(child) {
@@ -313,19 +344,53 @@ function waitForChildExit(child) {
});
}
function buildManagedSpawnOptions(env) {
function buildManagedSpawnOptions(env, { captureOutput = false } = {}) {
return {
cwd: repoRoot,
env,
stdio: 'inherit',
stdio: captureOutput ? ['inherit', 'pipe', 'pipe'] : 'inherit',
shell: process.platform === 'win32',
detached: process.platform !== 'win32',
windowsHide: process.platform === 'win32',
};
}
function spawnManaged(command, args, env) {
return spawn(command, args, buildManagedSpawnOptions(env));
function createTauriReadyPromise(child) {
return new Promise((resolveReady) => {
let ready = false;
let output = '';
const resolveOnce = () => {
if (!ready) {
ready = true;
resolveReady();
}
};
const forward = (stream, target) => {
stream?.on('data', (chunk) => {
target.write(chunk);
output = `${output}${chunk.toString()}`.slice(-4096);
if (output.includes(tauriDevReadyMarker)) {
resolveOnce();
}
});
};
forward(child.stdout, process.stdout);
forward(child.stderr, process.stderr);
child.once('error', resolveOnce);
child.once('exit', resolveOnce);
});
}
function spawnManaged(command, args, env, options = {}) {
const child = spawn(command, args, buildManagedSpawnOptions(env, options));
child.tauriReadyPromise = options.captureOutput
? createTauriReadyPromise(child)
: Promise.resolve();
return child;
}
function waitForTauriProcess(child) {
return child?.tauriReadyPromise ?? Promise.resolve();
}
function wait(ms) {
@@ -449,6 +514,7 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) {
const readState = dependencies.readState ?? (() => readDevStackState());
const waitForRoot = dependencies.waitForRoot ?? waitForRootStack;
const waitForAgc = dependencies.waitForAgc ?? waitForAgcFrontend;
const waitForTauri = dependencies.waitForTauri ?? waitForTauriProcess;
let rootChild = null;
let agcChild = null;
@@ -511,9 +577,12 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) {
}
console.log('[dev:all] 根开发栈已就绪,启动 AGC Vite 与 Tauri');
agcChild = spawnProcess(npmCommand, ['run', 'agc'], env);
agcChild = spawnProcess(npmCommand, ['run', 'agc'], env, {
captureOutput: true,
});
const agcExit = waitForChildExit(agcChild);
await waitForAgc(agcChild, endpoint);
await waitForTauri(agcChild);
if (rootChild.exitCode !== null || rootChild.signalCode !== null) {
throw new Error('根开发栈在 AGC 就绪前退出');
}
@@ -565,6 +634,7 @@ export {
buildStartupSummary,
isSharedRootStackReady,
readDevStackState,
renderStartupTable,
runDevAll,
validateDevAllArgs,
};
+15
View File
@@ -11,6 +11,7 @@ import {
buildRootDevArgs,
buildStartupSummary,
isSharedRootStackReady,
renderStartupTable,
runDevAll,
validateDevAllArgs,
} from './dev-all.mjs';
@@ -91,6 +92,9 @@ describe('dev:all 参数和共享开发栈合同', () => {
expect(options.shell).toBe(process.platform === 'win32');
expect(options.windowsHide).toBe(process.platform === 'win32');
expect(options.detached).toBe(process.platform !== 'win32');
expect(
buildManagedSpawnOptions(process.env, { captureOutput: true }).stdio,
).toEqual(['inherit', 'pipe', 'pipe']);
});
test('按最终端口构造完整启动摘要表', () => {
@@ -118,6 +122,17 @@ describe('dev:all 参数和共享开发栈合同', () => {
);
});
test('渲染摘要表时不输出字符串引号', () => {
const rows = buildStartupSummary(makeReadyState(), {
url: 'http://127.0.0.1:3080/',
});
const table = renderStartupTable(rows);
expect(table).not.toContain("'");
expect(table).toContain('| Web 主站');
expect(table).toContain('http://127.0.0.1:3080/');
});
test('仅接受同一工作树、实例、数据库和 data dir 的完整栈', () => {
const root = resolve('fixture-root');
const ready = makeReadyState(root);