visual-journal / scripts /doctor-hf-space.mjs
gpt-image-playground deploy
Deploy 912aa68 to Docker Space
704e84f
Raw
History Blame Contribute Delete
11.5 kB
#!/usr/bin/env node
import {
assertKnownOptions,
buildNextActions,
classifyRequiredAndRecommendedNames,
HF_SPACE_ID,
HF_SPACE_URL,
getJsonKeyValues,
getJsonNames,
isMainModule,
runDoctorCommand,
validateSpaceUrl
} from './hf-space-doctor-utils.mjs';
import { inspectDependencyInstallation } from './dependency-installation.mjs';
import { isSupportedNodeVersion, MIN_NODE_VERSION_RANGE } from './node-version.mjs';
import { inspectNpmInstallPolicy } from './npm-install-policy.mjs';
const REQUIRED_SPACE_VARIABLES = ['AGENT_STATE_BACKEND', 'NEXT_PUBLIC_IMAGE_STORAGE_MODE'];
const RECOMMENDED_SPACE_VARIABLES = ['APP_LOG_LEVEL'];
const REQUIRED_SPACE_VARIABLE_VALUES = new Map([
['AGENT_STATE_BACKEND', 'memory'],
['NEXT_PUBLIC_IMAGE_STORAGE_MODE', 'indexeddb']
]);
const REQUIRED_SPACE_SECRETS = ['APP_PASSWORD', 'AGENT_API_TOKEN'];
const OPTIONAL_GENERATION_SECRETS = ['OPENAI_API_KEY', 'OPENAI_CHANNEL_1_API_KEYS'];
function parseArgs(argv) {
assertKnownOptions(argv, ['--help', '-h', '--skip-remote']);
return {
help: argv.includes('--help') || argv.includes('-h'),
skipRemote: argv.includes('--skip-remote')
};
}
function printHelp() {
console.log(`Usage:
npm run doctor:hf-space
Options:
--skip-remote Skip read-only Hugging Face remote checks.
--help Show this help.`);
}
function addCheck(checks, status, name, message, details = {}) {
checks.push({ status, name, message, ...details });
}
function checkNode(checks) {
if (isSupportedNodeVersion()) {
addCheck(checks, 'pass', 'node', `Node.js ${process.version} is supported.`);
return;
}
addCheck(
checks,
'fail',
'node',
`Node.js ${process.version} is too old. Install Node.js ${MIN_NODE_VERSION_RANGE} or newer.`
);
}
function checkCommand(checks, name, command, args, failureAction) {
const result = runDoctorCommand(command, args);
if (result.ok) {
addCheck(checks, 'pass', name, `${command} is available.`, { version: result.stdout.split(/\r?\n/)[0] });
return true;
}
addCheck(checks, 'fail', name, failureAction, { error: result.error });
return false;
}
export function buildNpmInstallPolicyCheck(policy = inspectNpmInstallPolicy()) {
if (policy.ok) {
return {
status: 'pass',
name: 'npm-install-policy',
message: 'npm supports strict allowScripts installation policy.'
};
}
return {
status: 'fail',
name: 'npm-install-policy',
message: 'Current npm does not support --strict-allow-scripts; upgrade npm before installing dependencies.',
reason: policy.reason,
...(policy.error ? { error: policy.error } : {})
};
}
function checkConfiguredTarget(checks) {
const spaceUrlError = validateSpaceUrl(HF_SPACE_URL);
if (spaceUrlError) {
addCheck(checks, 'fail', 'space-target', spaceUrlError);
return;
}
addCheck(checks, 'pass', 'space-target', `Using fixed Space target ${HF_SPACE_ID}.`, { spaceUrl: HF_SPACE_URL });
}
function checkRemote(checks, skipRemote, hfAvailable, hfAuthenticated) {
if (skipRemote) {
addCheck(checks, 'skip', 'remote-space', 'Remote checks were skipped by --skip-remote.');
return;
}
if (!hfAvailable || !hfAuthenticated) {
addCheck(checks, 'skip', 'remote-space', 'Remote checks require hf CLI and hf auth login.');
return;
}
const info = runDoctorCommand('hf', ['spaces', 'info', HF_SPACE_ID, '--format', 'json']);
if (!info.ok) {
addCheck(checks, 'fail', 'remote-space', `Cannot read Space info for ${HF_SPACE_ID}.`, { error: info.error });
return;
}
addCheck(checks, 'pass', 'remote-space', `Space ${HF_SPACE_ID} is accessible.`);
checkRemoteNames(
checks,
HF_SPACE_ID,
'remote-variables',
['spaces', 'variables', 'list', HF_SPACE_ID, '--json'],
REQUIRED_SPACE_VARIABLES,
RECOMMENDED_SPACE_VARIABLES
);
checkRemoteSecrets(checks, HF_SPACE_ID);
}
function checkRemoteNames(checks, spaceId, name, args, requiredNames, recommendedNames = []) {
const result = runDoctorCommand('hf', args);
if (!result.ok) {
addCheck(checks, 'warn', name, `Cannot list ${name} for ${spaceId}.`, { error: result.error });
return;
}
try {
const names = getJsonNames(result.stdout);
const { missingRequired, missingRecommended } = classifyRequiredAndRecommendedNames(
names,
requiredNames,
recommendedNames
);
if (missingRequired.length) {
addCheck(checks, 'fail', name, `${name} missing required names: ${missingRequired.join(', ')}.`);
} else {
addCheck(checks, 'pass', name, `${name} contains required names.`);
}
if (missingRecommended.length) {
addCheck(checks, 'warn', name, `${name} missing recommended names: ${missingRecommended.join(', ')}.`);
}
checkRemoteVariableValues(checks, result.stdout);
} catch (error) {
addCheck(checks, 'warn', name, `Cannot parse ${name} JSON output.`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
function checkRemoteVariableValues(checks, jsonText) {
const values = getJsonKeyValues(jsonText);
const mismatches = [];
for (const [key, expected] of REQUIRED_SPACE_VARIABLE_VALUES) {
const actual = values.get(key);
if (actual !== expected) mismatches.push(`${key}=${actual ?? '<missing>'} expected ${expected}`);
}
if (mismatches.length) {
addCheck(
checks,
'fail',
'remote-variable-values',
`Remote variable values are not Space-free compatible: ${mismatches.join(', ')}.`
);
return;
}
addCheck(checks, 'pass', 'remote-variable-values', 'Remote variable values match the Space-free runtime contract.');
}
function checkRemoteSecrets(checks, spaceId) {
const result = runDoctorCommand('hf', ['spaces', 'secrets', 'list', spaceId, '--json']);
if (!result.ok) {
addCheck(checks, 'warn', 'remote-secrets', `Cannot list remote secrets for ${spaceId}.`, {
error: result.error
});
return;
}
try {
const names = getJsonNames(result.stdout);
const missing = REQUIRED_SPACE_SECRETS.filter((key) => !names.has(key));
const hasGenerationSecret = OPTIONAL_GENERATION_SECRETS.some((key) => names.has(key));
if (missing.length) {
addCheck(checks, 'fail', 'remote-secrets', `Remote secrets missing: ${missing.join(', ')}.`);
} else {
addCheck(checks, 'pass', 'remote-secrets', 'Remote secrets contain APP_PASSWORD and AGENT_API_TOKEN.');
}
if (hasGenerationSecret) {
addCheck(checks, 'pass', 'remote-generation-secret', 'Remote generation credential is configured.');
} else {
addCheck(
checks,
'warn',
'remote-generation-secret',
'No OPENAI_API_KEY or OPENAI_CHANNEL_1_API_KEYS secret found; server-side generation may be unavailable.'
);
}
} catch (error) {
addCheck(checks, 'warn', 'remote-secrets', 'Cannot parse remote secrets JSON output.', {
error: error instanceof Error ? error.message : String(error)
});
}
}
export function buildDependencyInstallationCheck(cwd = process.cwd()) {
const installation = inspectDependencyInstallation(cwd);
if (installation.ok) {
return { status: 'pass', name: 'node-modules', message: 'Direct dependencies match package-lock.json.' };
}
return {
status: 'warn',
name: 'node-modules',
message:
'node_modules is missing or incomplete; build, lint, test, and smoke commands require npm run install-scripts:check && npm run npm-install-policy:check && npm ci --strict-allow-scripts && npm run dependencies:check.',
reason: installation.reason,
...(installation.missingPackages.length ? { missingPackages: installation.missingPackages } : {}),
...(installation.invalidPackages.length ? { invalidPackages: installation.invalidPackages } : {}),
...(installation.nameMismatches.length ? { nameMismatches: installation.nameMismatches } : {}),
...(installation.versionMismatches.length ? { versionMismatches: installation.versionMismatches } : {}),
...(installation.rootLockMismatches.length ? { rootLockMismatches: installation.rootLockMismatches } : {}),
...(installation.hiddenLockMismatches.length ? { hiddenLockMismatches: installation.hiddenLockMismatches } : {})
};
}
function checkDependencyInstallation(checks) {
checks.push(buildDependencyInstallationCheck());
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const checks = [];
checkNode(checks);
const npmAvailable = checkCommand(
checks,
'npm',
'npm',
['--version'],
`npm is missing. Install Node.js ${MIN_NODE_VERSION_RANGE} or newer with npm.`
);
if (npmAvailable) checks.push(buildNpmInstallPolicyCheck());
else addCheck(checks, 'skip', 'npm-install-policy', 'npm install policy check requires npm.');
const hfAvailable = checkCommand(
checks,
'hf-cli',
'hf',
['version'],
'hf CLI is missing. Install the Hugging Face CLI.'
);
const hfAuth = hfAvailable ? runDoctorCommand('hf', ['auth', 'whoami']) : { ok: false };
if (hfAvailable && hfAuth.ok) {
addCheck(checks, 'pass', 'hf-auth', 'hf CLI is authenticated.');
} else if (hfAvailable) {
addCheck(checks, 'fail', 'hf-auth', 'hf CLI auth check failed.', {
action: 'hf auth login',
error: hfAuth.error
});
}
checkDependencyInstallation(checks);
checkCommand(
checks,
'git',
'git',
['--version'],
'git is missing; install git before cloning or pushing Space repos.'
);
const docker = runDoctorCommand('docker', ['version', '--format', '{{.Server.Version}}']);
if (docker.ok) {
addCheck(checks, 'pass', 'docker', 'docker is available.', { version: docker.stdout.split(/\r?\n/)[0] });
} else {
addCheck(checks, 'warn', 'docker', 'Docker is unavailable; npm run smoke:hf-space-local will not work.', {
error: docker.error
});
const dockerCli = runDoctorCommand('docker', ['--version']);
if (dockerCli.ok)
addCheck(checks, 'warn', 'docker-daemon', 'Docker CLI exists but the daemon is not reachable.');
}
checkConfiguredTarget(checks);
checkRemote(checks, options.skipRemote, hfAvailable, Boolean(hfAuth.ok));
const failed = checks.some((check) => check.status === 'fail');
console.log(JSON.stringify({ ok: !failed, checks, nextActions: buildNextActions(checks) }, null, 2));
if (failed) process.exit(1);
}
try {
if (isMainModule(import.meta.url, process.argv[1])) {
main();
}
} catch (error) {
console.error(
JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }, null, 2)
);
process.exit(1);
}