2026-03-14 22:51:55 +01:00
|
|
|
import { spawnSync } from 'child_process';
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
|
|
|
|
|
import { AGENT_DOMAIN, AGENT_NAME } from '../src/config.js';
|
|
|
|
|
import { logger } from '../src/logger.js';
|
|
|
|
|
import { emitStatus } from './status.js';
|
|
|
|
|
|
|
|
|
|
interface PreflightArgs {
|
|
|
|
|
withOnboarding: boolean;
|
|
|
|
|
capturePasswordStep: boolean;
|
|
|
|
|
failFast: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StepDefinition {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
command: string;
|
|
|
|
|
args: string[];
|
2026-03-14 23:04:03 +01:00
|
|
|
interactive?: boolean;
|
2026-03-14 22:51:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StepResult {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
commandLine: string;
|
|
|
|
|
exitCode: number;
|
|
|
|
|
status: 'success' | 'failed' | 'skipped';
|
|
|
|
|
startedAt: string;
|
|
|
|
|
finishedAt: string;
|
|
|
|
|
logFile: string;
|
|
|
|
|
fields: Record<string, string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PasswordCaptureResult {
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
captured: boolean;
|
|
|
|
|
published: boolean;
|
|
|
|
|
captureDir: string;
|
|
|
|
|
publishDir: string;
|
|
|
|
|
logFile: string;
|
|
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseArgs(args: string[]): PreflightArgs {
|
|
|
|
|
const result: PreflightArgs = {
|
|
|
|
|
withOnboarding: false,
|
|
|
|
|
capturePasswordStep: false,
|
|
|
|
|
failFast: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const arg of args) {
|
|
|
|
|
if (arg === '--with-onboarding') result.withOnboarding = true;
|
|
|
|
|
if (arg === '--capture-password-step') result.capturePasswordStep = true;
|
|
|
|
|
if (arg === '--fail-fast') result.failFast = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.capturePasswordStep) {
|
|
|
|
|
result.withOnboarding = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDisplayTimestamp(date: Date): string {
|
|
|
|
|
return date.toLocaleString('de-DE', {
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
second: '2-digit',
|
|
|
|
|
hour12: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatRunStamp(date: Date): string {
|
|
|
|
|
const pad = (value: number) => String(value).padStart(2, '0');
|
|
|
|
|
return [
|
|
|
|
|
date.getFullYear(),
|
|
|
|
|
pad(date.getMonth() + 1),
|
|
|
|
|
pad(date.getDate()),
|
|
|
|
|
'-',
|
|
|
|
|
pad(date.getHours()),
|
|
|
|
|
pad(date.getMinutes()),
|
|
|
|
|
pad(date.getSeconds()),
|
|
|
|
|
].join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quoteShell(arg: string): string {
|
|
|
|
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(arg)) {
|
|
|
|
|
return arg;
|
|
|
|
|
}
|
|
|
|
|
return JSON.stringify(arg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseStatusFields(output: string): Record<string, string> {
|
|
|
|
|
const matches = output.match(/=== CLAWDIE SETUP: [\s\S]*?=== END ===/gu) || [];
|
|
|
|
|
const block = matches.at(-1);
|
|
|
|
|
if (!block) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fields: Record<string, string> = {};
|
|
|
|
|
for (const line of block.split('\n')) {
|
|
|
|
|
if (line.startsWith('=== ')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const separator = line.indexOf(':');
|
|
|
|
|
if (separator === -1) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const key = line.slice(0, separator).trim();
|
|
|
|
|
const value = line.slice(separator + 1).trim();
|
|
|
|
|
if (key) {
|
|
|
|
|
fields[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return fields;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readEnvValue(projectRoot: string, key: string): string | null {
|
|
|
|
|
const envFile = path.join(projectRoot, '.env');
|
|
|
|
|
if (!fs.existsSync(envFile)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const content = fs.readFileSync(envFile, 'utf-8');
|
|
|
|
|
const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));
|
|
|
|
|
return match ? match[1].trim().replace(/^['"]|['"]$/gu, '') : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSteps(opts: PreflightArgs): StepDefinition[] {
|
|
|
|
|
const steps: StepDefinition[] = [];
|
|
|
|
|
|
|
|
|
|
if (opts.withOnboarding) {
|
|
|
|
|
steps.push({
|
|
|
|
|
id: 'onboarding',
|
|
|
|
|
label: 'Onboarding',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'wizard'],
|
2026-03-14 23:04:03 +01:00
|
|
|
interactive: true,
|
2026-03-14 22:51:55 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
steps.push(
|
|
|
|
|
{
|
|
|
|
|
id: 'environment',
|
|
|
|
|
label: 'Environment',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'environment'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pi-config',
|
|
|
|
|
label: 'PI Config',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'pi-config'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'jails',
|
|
|
|
|
label: 'Jails',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'jails', '--create'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'db',
|
|
|
|
|
label: 'DB',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'db'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'git',
|
|
|
|
|
label: 'Git',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'git'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'cms',
|
|
|
|
|
label: 'CMS',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'cms'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'hosts',
|
|
|
|
|
label: 'Hosts',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'hosts'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'mounts',
|
|
|
|
|
label: 'Mounts',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'mounts'],
|
|
|
|
|
},
|
2026-03-14 23:04:03 +01:00
|
|
|
{
|
|
|
|
|
id: 'telegram-auth',
|
|
|
|
|
label: 'Telegram Auth',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'telegram-auth'],
|
|
|
|
|
},
|
2026-03-14 22:51:55 +01:00
|
|
|
{
|
|
|
|
|
id: 'service',
|
|
|
|
|
label: 'Service',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'service'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'verify',
|
|
|
|
|
label: 'Verify',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'setup', '--', '--step', 'verify'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'doctor',
|
|
|
|
|
label: 'Doctor',
|
|
|
|
|
command: 'npm',
|
|
|
|
|
args: ['run', 'doctor'],
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return steps;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runStep(
|
|
|
|
|
projectRoot: string,
|
|
|
|
|
runDir: string,
|
|
|
|
|
step: StepDefinition,
|
|
|
|
|
): StepResult {
|
|
|
|
|
const started = new Date();
|
|
|
|
|
const commandLine = [step.command, ...step.args].map(quoteShell).join(' ');
|
2026-03-14 23:04:03 +01:00
|
|
|
const logFile = path.join(runDir, `${step.id}.log`);
|
|
|
|
|
const interactive = step.interactive === true;
|
|
|
|
|
|
|
|
|
|
if (interactive && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
|
|
|
const output = 'interactive_tty_required\n';
|
|
|
|
|
fs.writeFileSync(logFile, output);
|
2026-03-14 23:23:04 +01:00
|
|
|
const finished = new Date();
|
2026-03-14 23:04:03 +01:00
|
|
|
return {
|
|
|
|
|
id: step.id,
|
|
|
|
|
label: step.label,
|
|
|
|
|
commandLine,
|
|
|
|
|
exitCode: 1,
|
|
|
|
|
status: 'failed',
|
|
|
|
|
startedAt: started.toISOString(),
|
|
|
|
|
finishedAt: finished.toISOString(),
|
|
|
|
|
logFile,
|
|
|
|
|
fields: { ERROR: 'interactive_tty_required' },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (interactive) {
|
|
|
|
|
const child = spawnSync(step.command, step.args, {
|
|
|
|
|
cwd: projectRoot,
|
|
|
|
|
env: process.env,
|
|
|
|
|
stdio: 'inherit',
|
|
|
|
|
});
|
|
|
|
|
fs.writeFileSync(logFile, '[interactive output inherited to terminal]\n');
|
2026-03-14 23:23:04 +01:00
|
|
|
const finished = new Date();
|
2026-03-14 23:04:03 +01:00
|
|
|
return {
|
|
|
|
|
id: step.id,
|
|
|
|
|
label: step.label,
|
|
|
|
|
commandLine,
|
|
|
|
|
exitCode: child.status ?? 1,
|
|
|
|
|
status: (child.status ?? 1) === 0 ? 'success' : 'failed',
|
|
|
|
|
startedAt: started.toISOString(),
|
|
|
|
|
finishedAt: finished.toISOString(),
|
|
|
|
|
logFile,
|
|
|
|
|
fields: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 22:51:55 +01:00
|
|
|
const child = spawnSync(step.command, step.args, {
|
|
|
|
|
cwd: projectRoot,
|
|
|
|
|
encoding: 'utf-8',
|
|
|
|
|
env: process.env,
|
|
|
|
|
});
|
|
|
|
|
const output = [
|
|
|
|
|
child.stdout || '',
|
|
|
|
|
child.stderr || '',
|
|
|
|
|
child.error ? String(child.error) : '',
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n');
|
|
|
|
|
fs.writeFileSync(logFile, output || '\n');
|
2026-03-14 23:23:04 +01:00
|
|
|
const finished = new Date();
|
2026-03-14 22:51:55 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: step.id,
|
|
|
|
|
label: step.label,
|
|
|
|
|
commandLine,
|
|
|
|
|
exitCode: child.status ?? 1,
|
|
|
|
|
status: (child.status ?? 1) === 0 ? 'success' : 'failed',
|
|
|
|
|
startedAt: started.toISOString(),
|
|
|
|
|
finishedAt: finished.toISOString(),
|
|
|
|
|
logFile,
|
|
|
|
|
fields: parseStatusFields(output),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function capturePasswordStep(
|
|
|
|
|
projectRoot: string,
|
|
|
|
|
runDir: string,
|
|
|
|
|
): PasswordCaptureResult {
|
|
|
|
|
const captureDir = path.join(runDir, 'password-step-screenshot');
|
|
|
|
|
const logFile = path.join(runDir, 'password-step-screenshot.log');
|
|
|
|
|
const child = spawnSync(
|
|
|
|
|
'python3',
|
|
|
|
|
[
|
|
|
|
|
path.join(
|
|
|
|
|
projectRoot,
|
|
|
|
|
'.agent',
|
|
|
|
|
'skills',
|
|
|
|
|
'tmux-screenshot',
|
|
|
|
|
'tmux-screenshot.py',
|
|
|
|
|
),
|
|
|
|
|
'--session',
|
|
|
|
|
'clawdie',
|
|
|
|
|
'--window',
|
|
|
|
|
'main',
|
|
|
|
|
'--outdir',
|
|
|
|
|
captureDir,
|
|
|
|
|
],
|
|
|
|
|
{
|
|
|
|
|
cwd: projectRoot,
|
|
|
|
|
encoding: 'utf-8',
|
|
|
|
|
env: process.env,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
const output = [child.stdout || '', child.stderr || ''].filter(Boolean).join('\n');
|
|
|
|
|
const fullOutput = [
|
|
|
|
|
output,
|
|
|
|
|
child.error ? String(child.error) : '',
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n');
|
|
|
|
|
fs.writeFileSync(logFile, fullOutput || '\n');
|
|
|
|
|
|
|
|
|
|
if ((child.status ?? 1) !== 0) {
|
|
|
|
|
return {
|
|
|
|
|
enabled: true,
|
|
|
|
|
captured: false,
|
|
|
|
|
published: false,
|
|
|
|
|
captureDir,
|
|
|
|
|
publishDir: '',
|
|
|
|
|
logFile,
|
|
|
|
|
error: 'capture_failed',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
enabled: true,
|
|
|
|
|
captured: true,
|
|
|
|
|
published: false,
|
|
|
|
|
captureDir,
|
|
|
|
|
publishDir: '',
|
|
|
|
|
logFile,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function publishPasswordCapture(
|
|
|
|
|
projectRoot: string,
|
|
|
|
|
passwordCapture: PasswordCaptureResult,
|
|
|
|
|
): PasswordCaptureResult {
|
|
|
|
|
const publishDir = path.join(
|
|
|
|
|
'/usr/local/bastille/jails',
|
|
|
|
|
readEnvValue(projectRoot, 'CMS_JAIL_NAME') || `${AGENT_NAME}-cms`,
|
|
|
|
|
'root',
|
|
|
|
|
'srv',
|
|
|
|
|
'www',
|
|
|
|
|
'screenshots',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
fs.mkdirSync(publishDir, { recursive: true });
|
|
|
|
|
for (const entry of fs.readdirSync(passwordCapture.captureDir)) {
|
|
|
|
|
fs.cpSync(path.join(passwordCapture.captureDir, entry), path.join(publishDir, entry), {
|
|
|
|
|
recursive: true,
|
|
|
|
|
force: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...passwordCapture,
|
|
|
|
|
published: true,
|
|
|
|
|
publishDir,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
...passwordCapture,
|
|
|
|
|
published: false,
|
|
|
|
|
publishDir,
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writeSummaryFiles(
|
|
|
|
|
runDir: string,
|
|
|
|
|
args: PreflightArgs,
|
|
|
|
|
results: StepResult[],
|
|
|
|
|
passwordCapture: PasswordCaptureResult | null,
|
|
|
|
|
): void {
|
|
|
|
|
const overallStatus =
|
|
|
|
|
results.every((result) => result.status === 'success') &&
|
|
|
|
|
(!passwordCapture ||
|
|
|
|
|
(!passwordCapture.enabled ||
|
|
|
|
|
(passwordCapture.captured && passwordCapture.published)))
|
|
|
|
|
? 'success'
|
|
|
|
|
: 'failed';
|
|
|
|
|
|
|
|
|
|
const summaryJson = {
|
|
|
|
|
generatedAt: new Date().toISOString(),
|
|
|
|
|
generatedAtDisplay: formatDisplayTimestamp(new Date()),
|
|
|
|
|
overallStatus,
|
|
|
|
|
args,
|
|
|
|
|
passwordCapture,
|
|
|
|
|
results: results.map((result) => ({
|
|
|
|
|
...result,
|
|
|
|
|
logFile: path.relative(runDir, result.logFile),
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(runDir, 'summary.json'),
|
|
|
|
|
`${JSON.stringify(summaryJson, null, 2)}\n`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const envLines = [
|
|
|
|
|
`OVERALL_STATUS=${overallStatus}`,
|
|
|
|
|
`WITH_ONBOARDING=${args.withOnboarding}`,
|
|
|
|
|
`CAPTURE_PASSWORD_STEP=${args.capturePasswordStep}`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (passwordCapture) {
|
|
|
|
|
envLines.push(`PASSWORD_STEP_CAPTURE_ENABLED=${passwordCapture.enabled}`);
|
|
|
|
|
envLines.push(`PASSWORD_STEP_CAPTURED=${passwordCapture.captured}`);
|
|
|
|
|
envLines.push(`PASSWORD_STEP_PUBLISHED=${passwordCapture.published}`);
|
|
|
|
|
envLines.push(`PASSWORD_STEP_CAPTURE_DIR=${passwordCapture.captureDir}`);
|
|
|
|
|
envLines.push(`PASSWORD_STEP_PUBLISH_DIR=${passwordCapture.publishDir}`);
|
|
|
|
|
envLines.push(`PASSWORD_STEP_LOG=${passwordCapture.logFile}`);
|
|
|
|
|
if (passwordCapture.error) {
|
|
|
|
|
envLines.push(`PASSWORD_STEP_ERROR=${JSON.stringify(passwordCapture.error)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const result of results) {
|
|
|
|
|
const prefix = result.id.toUpperCase().replace(/[^A-Z0-9]+/gu, '_');
|
|
|
|
|
envLines.push(`${prefix}_STATUS=${result.status}`);
|
|
|
|
|
envLines.push(`${prefix}_EXIT_CODE=${result.exitCode}`);
|
|
|
|
|
envLines.push(`${prefix}_LOG=${result.logFile}`);
|
|
|
|
|
for (const [key, value] of Object.entries(result.fields)) {
|
|
|
|
|
const envKey = `${prefix}_${key.replace(/[^A-Z0-9]+/giu, '_').toUpperCase()}`;
|
|
|
|
|
envLines.push(`${envKey}=${JSON.stringify(value)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(runDir, 'summary.env'), `${envLines.join('\n')}\n`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(args: string[]): Promise<void> {
|
|
|
|
|
const projectRoot = process.cwd();
|
|
|
|
|
const opts = parseArgs(args);
|
|
|
|
|
const started = new Date();
|
|
|
|
|
const runDir = path.join(projectRoot, 'tmp', 'preflight', formatRunStamp(started));
|
|
|
|
|
const steps = buildSteps(opts);
|
|
|
|
|
const results: StepResult[] = [];
|
|
|
|
|
let passwordCapture: PasswordCaptureResult | null = null;
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(runDir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
console.log(`Preflight started: ${formatDisplayTimestamp(started)}`);
|
|
|
|
|
console.log(`Run directory: ${runDir}`);
|
|
|
|
|
console.log(`Public domain: ${AGENT_DOMAIN}`);
|
|
|
|
|
console.log('');
|
|
|
|
|
|
|
|
|
|
for (const step of steps) {
|
|
|
|
|
console.log(`[preflight] ${step.label} -> ${[step.command, ...step.args].join(' ')}`);
|
|
|
|
|
const result = runStep(projectRoot, runDir, step);
|
|
|
|
|
results.push(result);
|
|
|
|
|
|
2026-03-14 23:23:04 +01:00
|
|
|
if (step.id === 'onboarding' && opts.capturePasswordStep && result.status === 'success') {
|
2026-03-14 22:51:55 +01:00
|
|
|
passwordCapture = capturePasswordStep(projectRoot, runDir);
|
|
|
|
|
}
|
|
|
|
|
if (step.id === 'cms' && passwordCapture?.captured && !passwordCapture.published) {
|
|
|
|
|
passwordCapture = publishPasswordCapture(projectRoot, passwordCapture);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.status === 'success') {
|
|
|
|
|
console.log(` ok ${step.label} (${path.relative(projectRoot, result.logFile)})`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(` fail ${step.label} (${path.relative(projectRoot, result.logFile)})`);
|
|
|
|
|
if (opts.failFast) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeSummaryFiles(runDir, opts, results, passwordCapture);
|
|
|
|
|
|
|
|
|
|
const overallStatus =
|
|
|
|
|
results.every((result) => result.status === 'success') &&
|
|
|
|
|
(!passwordCapture ||
|
|
|
|
|
(!passwordCapture.enabled ||
|
|
|
|
|
(passwordCapture.captured && passwordCapture.published)))
|
|
|
|
|
? 'success'
|
|
|
|
|
: 'failed';
|
|
|
|
|
|
|
|
|
|
logger.info({ runDir, overallStatus }, 'Preflight check complete');
|
|
|
|
|
emitStatus('PREFLIGHT', {
|
|
|
|
|
RUN_DIR: path.relative(projectRoot, runDir),
|
|
|
|
|
OVERALL_STATUS: overallStatus,
|
|
|
|
|
WITH_ONBOARDING: opts.withOnboarding,
|
|
|
|
|
CAPTURE_PASSWORD_STEP: opts.capturePasswordStep,
|
|
|
|
|
SUMMARY_JSON: path.relative(projectRoot, path.join(runDir, 'summary.json')),
|
|
|
|
|
SUMMARY_ENV: path.relative(projectRoot, path.join(runDir, 'summary.env')),
|
|
|
|
|
STATUS: overallStatus,
|
|
|
|
|
LOG: path.relative(projectRoot, runDir),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (overallStatus !== 'success') {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|