Spaces:
Running
Running
| import { MIN_NODE_VERSION_RANGE } from './node-version.mjs'; | |
| import { spawnSync } from 'node:child_process'; | |
| import { resolve } from 'node:path'; | |
| import { pathToFileURL } from 'node:url'; | |
| export const HF_SPACE_ID = 'misonL/visual-journal'; | |
| export const HF_SPACE_URL = 'https://misonl-visual-journal.hf.space'; | |
| const DOCTOR_COMMAND_TIMEOUT_MS = 30_000; | |
| export function readEnvValue(name) { | |
| return process.env[name]?.trim() || undefined; | |
| } | |
| export function readOptionValue(argv, name) { | |
| const prefix = `${name}=`; | |
| const inlineValue = argv.find((arg) => arg.startsWith(prefix)); | |
| if (inlineValue !== undefined) { | |
| const value = inlineValue.slice(prefix.length).trim(); | |
| if (!value) throw new Error(`${name} requires a value`); | |
| return value; | |
| } | |
| const index = argv.indexOf(name); | |
| if (index === -1) return undefined; | |
| const value = argv[index + 1]?.trim(); | |
| if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`); | |
| return value; | |
| } | |
| export function assertKnownOptions(argv, knownOptions) { | |
| const known = new Set(knownOptions); | |
| for (const arg of argv) { | |
| if (!arg.startsWith('-')) continue; | |
| const name = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg; | |
| if (!known.has(name)) throw new Error(`Unknown option: ${name}`); | |
| } | |
| } | |
| export function runDoctorCommand(command, args = [], options = {}) { | |
| const result = spawnSync(command, args, { | |
| encoding: 'utf8', | |
| stdio: ['ignore', 'pipe', 'pipe'], | |
| timeout: options.timeoutMs || DOCTOR_COMMAND_TIMEOUT_MS | |
| }); | |
| if (result.error) { | |
| const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim(); | |
| return { ok: false, error: output ? `${result.error.message}\n${output}` : result.error.message }; | |
| } | |
| if (result.status !== 0) { | |
| const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim(); | |
| return { ok: false, error: output || `${command} ${args.join(' ')} failed` }; | |
| } | |
| return { ok: true, stdout: result.stdout.trim() }; | |
| } | |
| export function getJsonNames(text) { | |
| const parsed = JSON.parse(extractJsonPayload(text)); | |
| const names = new Set(); | |
| const visit = (value) => { | |
| if (Array.isArray(value)) { | |
| for (const item of value) visit(item); | |
| return; | |
| } | |
| if (!value || typeof value !== 'object') return; | |
| for (const key of ['name', 'key', 'id']) { | |
| if (typeof value[key] === 'string') names.add(value[key]); | |
| } | |
| }; | |
| visit(parsed); | |
| return names; | |
| } | |
| export function getJsonKeyValues(text) { | |
| const parsed = JSON.parse(extractJsonPayload(text)); | |
| const values = new Map(); | |
| const visit = (value) => { | |
| if (Array.isArray(value)) { | |
| for (const item of value) visit(item); | |
| return; | |
| } | |
| if (!value || typeof value !== 'object') return; | |
| if (typeof value.key === 'string' && typeof value.value === 'string') { | |
| values.set(value.key, value.value); | |
| } | |
| }; | |
| visit(parsed); | |
| return values; | |
| } | |
| function extractJsonPayload(text) { | |
| const trimmedText = text.trim(); | |
| if (!trimmedText) return '[]'; | |
| const lines = trimmedText.split(/\r?\n/); | |
| const jsonLineIndex = lines.findIndex((line) => { | |
| const trimmedLine = line.trimStart(); | |
| return trimmedLine.startsWith('{') || trimmedLine.startsWith('['); | |
| }); | |
| if (jsonLineIndex === -1) return trimmedText; | |
| return lines.slice(jsonLineIndex).join('\n'); | |
| } | |
| export function missingKeys(keys, values) { | |
| return keys.filter((key) => !values.has(key) || !String(values.get(key)).trim()); | |
| } | |
| export function classifyRequiredAndRecommendedNames(names, requiredNames, recommendedNames = []) { | |
| return { | |
| missingRequired: requiredNames.filter((key) => !names.has(key)), | |
| missingRecommended: recommendedNames.filter((key) => !names.has(key)) | |
| }; | |
| } | |
| const NEXT_ACTIONS = new Map([ | |
| ['node', `Install Node.js ${MIN_NODE_VERSION_RANGE}, then reopen the terminal.`], | |
| ['npm', `Install Node.js ${MIN_NODE_VERSION_RANGE} with npm.`], | |
| [ | |
| 'hf-cli', | |
| 'Install the Hugging Face CLI from the official documentation; avoid piping remote install scripts directly to a shell.' | |
| ], | |
| [ | |
| 'hf-auth', | |
| 'Check network/proxy access to Hugging Face, then run hf auth login if the token is missing or expired.' | |
| ], | |
| ['npm-install-policy', 'Upgrade npm to a version that supports --strict-allow-scripts, then reopen the terminal.'], | |
| [ | |
| 'node-modules', | |
| 'Run npm run install-scripts:check && npm run npm-install-policy:check && npm ci --strict-allow-scripts && npm run dependencies:check.' | |
| ], | |
| ['remote-variables', 'Configure the required and recommended Space Variables with hf spaces variables add.'], | |
| ['remote-variable-values', 'Set required Space Variable values with hf spaces variables add.'], | |
| ['remote-secrets', 'Configure required Space Secrets with hf spaces secrets add.'], | |
| [ | |
| 'remote-generation-secret', | |
| 'Configure OPENAI_API_KEY or OPENAI_CHANNEL_1_API_KEYS in Space Secrets before real image generation.' | |
| ] | |
| ]); | |
| export function buildNextActions(checks) { | |
| const actions = new Set(); | |
| for (const check of checks) { | |
| if (check.status === 'pass' || check.status === 'skip') continue; | |
| if (check.action) actions.add(check.action); | |
| const mappedAction = NEXT_ACTIONS.get(check.name); | |
| if (mappedAction) actions.add(mappedAction); | |
| } | |
| return [...actions]; | |
| } | |
| export function validateSpaceId(spaceId) { | |
| const text = spaceId?.trim(); | |
| if (!text) return 'HF Space id is required.'; | |
| const parts = text.split('/'); | |
| if (parts.length < 1 || parts.length > 2 || parts.some((part) => part.length === 0)) { | |
| return 'HF Space id must use space_name or namespace/space_name format with 1-96 characters per part.'; | |
| } | |
| for (const part of parts) { | |
| if (part.length > 96 || !/^[A-Za-z0-9._-]+$/.test(part)) { | |
| return 'HF Space id must use space_name or namespace/space_name format with 1-96 characters per part.'; | |
| } | |
| if (part.startsWith('.') || part.includes('..') || part.includes('--') || part.endsWith('.git')) { | |
| return 'HF Space id parts cannot start with ".", contain ".." or "--", or end with ".git".'; | |
| } | |
| } | |
| return undefined; | |
| } | |
| export function validateSpaceUrl(spaceUrl) { | |
| if (!spaceUrl?.trim()) return 'HF Space URL is required.'; | |
| let parsedUrl; | |
| try { | |
| parsedUrl = new URL(spaceUrl); | |
| } catch { | |
| return 'HF Space URL is not a valid URL.'; | |
| } | |
| if (parsedUrl.protocol !== 'https:') return 'HF Space URL must use https.'; | |
| if (!parsedUrl.hostname.endsWith('.hf.space')) return 'HF Space URL must be a Hugging Face .hf.space URL.'; | |
| if (parsedUrl.username || parsedUrl.password || parsedUrl.pathname !== '/' || parsedUrl.search || parsedUrl.hash) { | |
| return 'HF Space URL must be a plain Space origin without credentials, path, query, or fragment.'; | |
| } | |
| return undefined; | |
| } | |
| export function assertSpaceTargetConfig({ spaceId, spaceUrl }) { | |
| const spaceIdError = validateSpaceId(spaceId); | |
| if (spaceIdError) throw new Error(spaceIdError); | |
| const spaceUrlError = validateSpaceUrl(spaceUrl); | |
| if (spaceUrlError) throw new Error(spaceUrlError); | |
| } | |
| export function isMainModule(moduleUrl, argvPath) { | |
| return Boolean(argvPath) && moduleUrl === pathToFileURL(resolve(argvPath)).href; | |
| } | |