Spaces:
Running
Running
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import { spawnSync } from 'node:child_process'; | |
| import { pathToFileURL } from 'node:url'; | |
| const ROOT_DIR = path.resolve(import.meta.dirname, '..'); | |
| const DEFAULT_SOURCE_DIR = path.join(ROOT_DIR, 'dist', 'hf-space'); | |
| const DEFAULT_BRANCH = 'main'; | |
| export function parseArgs(argv) { | |
| const options = { | |
| sourceDir: DEFAULT_SOURCE_DIR, | |
| branch: DEFAULT_BRANCH, | |
| remote: '', | |
| skipPrepare: false, | |
| force: false, | |
| unsafeSource: false | |
| }; | |
| for (let index = 0; index < argv.length; index += 1) { | |
| const item = argv[index]; | |
| if (item === '--remote') { | |
| const value = argv[index + 1]; | |
| if (!value) { | |
| throw new Error('--remote requires a git remote URL.'); | |
| } | |
| options.remote = value; | |
| index += 1; | |
| continue; | |
| } | |
| if (item.startsWith('--remote=')) { | |
| options.remote = item.slice('--remote='.length); | |
| continue; | |
| } | |
| if (item === '--branch') { | |
| const value = argv[index + 1]; | |
| if (!value) { | |
| throw new Error('--branch requires a branch name.'); | |
| } | |
| options.branch = value; | |
| index += 1; | |
| continue; | |
| } | |
| if (item.startsWith('--branch=')) { | |
| options.branch = item.slice('--branch='.length); | |
| continue; | |
| } | |
| if (item === '--source') { | |
| const value = argv[index + 1]; | |
| if (!value) { | |
| throw new Error('--source requires a directory path.'); | |
| } | |
| options.sourceDir = path.resolve(ROOT_DIR, value); | |
| index += 1; | |
| continue; | |
| } | |
| if (item.startsWith('--source=')) { | |
| options.sourceDir = path.resolve(ROOT_DIR, item.slice('--source='.length)); | |
| continue; | |
| } | |
| if (item === '--skip-prepare') { | |
| options.skipPrepare = true; | |
| continue; | |
| } | |
| if (item === '--force') { | |
| options.force = true; | |
| continue; | |
| } | |
| if (item === '--unsafe-source') { | |
| options.unsafeSource = true; | |
| continue; | |
| } | |
| throw new Error(`Unknown argument: ${item}`); | |
| } | |
| if (!options.force) { | |
| throw new Error('--force is required because deployment rewrites the target branch.'); | |
| } | |
| assertSourceInsideRepo(options.sourceDir, options.unsafeSource); | |
| return options; | |
| } | |
| export function assertSourceInsideRepo(sourceDir, unsafeSource = false) { | |
| const resolvedLexical = path.resolve(sourceDir); | |
| const lexicalRelative = path.relative(ROOT_DIR, resolvedLexical); | |
| if (lexicalRelative === '') { | |
| throw new Error('source must not be the repository root.'); | |
| } | |
| const existingPath = nearestExistingPath(resolvedLexical); | |
| assertSourceNotRepositoryRoot(existingPath, existingPath === resolvedLexical); | |
| if (unsafeSource) { | |
| return; | |
| } | |
| if (isOutsideRelativePath(lexicalRelative)) { | |
| throw new Error('source must stay inside the repository unless --unsafe-source is set.'); | |
| } | |
| assertSourceRealpathInsideRepo(existingPath, existingPath === resolvedLexical); | |
| } | |
| export function assertDeploySourceReady(sourceDir, unsafeSource = false) { | |
| assertSourceInsideRepo(sourceDir, unsafeSource); | |
| assertSourceDir(sourceDir); | |
| } | |
| function isOutsideRelativePath(relativePath) { | |
| return relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath); | |
| } | |
| function nearestExistingPath(targetPath) { | |
| let current = targetPath; | |
| while (true) { | |
| try { | |
| fs.lstatSync(current); | |
| return current; | |
| } catch (error) { | |
| if (error.code !== 'ENOENT') { | |
| throw error; | |
| } | |
| const parent = path.dirname(current); | |
| if (parent === current) { | |
| throw error; | |
| } | |
| current = parent; | |
| } | |
| } | |
| } | |
| function assertSourceRealpathInsideRepo(sourceDir, rejectRepositoryRoot = false) { | |
| const { resolvedRoot, resolvedSource } = resolveRootAndSource(sourceDir); | |
| const relative = path.relative(resolvedRoot, resolvedSource); | |
| if (relative === '' && rejectRepositoryRoot) { | |
| throw new Error('source must not be the repository root.'); | |
| } | |
| if (relative === '' && path.resolve(sourceDir) !== ROOT_DIR) { | |
| throw new Error('source must not be the repository root.'); | |
| } | |
| if (isOutsideRelativePath(relative)) { | |
| throw new Error('source must stay inside the repository unless --unsafe-source is set.'); | |
| } | |
| } | |
| function assertSourceNotRepositoryRoot(sourceDir, rejectRepositoryRoot = false) { | |
| const { resolvedRoot, resolvedSource } = resolveRootAndSource(sourceDir); | |
| const relative = path.relative(resolvedRoot, resolvedSource); | |
| if (relative === '' && (rejectRepositoryRoot || path.resolve(sourceDir) !== ROOT_DIR)) { | |
| throw new Error('source must not be the repository root.'); | |
| } | |
| } | |
| function resolveRootAndSource(sourceDir) { | |
| let resolvedRoot = ''; | |
| let resolvedSource = ''; | |
| try { | |
| resolvedRoot = fs.realpathSync(ROOT_DIR); | |
| resolvedSource = fs.realpathSync(sourceDir); | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| throw new Error(`Source path does not exist: ${sourceDir}`); | |
| } | |
| throw error; | |
| } | |
| return { resolvedRoot, resolvedSource }; | |
| } | |
| function normalizeRemote(remote) { | |
| const value = String(remote || '').trim(); | |
| if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || /^[^/]+@[^:]+:.+/.test(value) || path.isAbsolute(value)) { | |
| return value; | |
| } | |
| return path.resolve(ROOT_DIR, value); | |
| } | |
| function assertHuggingFaceSpaceRemote(remote) { | |
| const value = String(remote || '').trim(); | |
| if (/^https:\/\/huggingface\.co\/spaces\/[^/\s]+\/[^/\s]+(?:\.git)?$/i.test(value)) { | |
| return; | |
| } | |
| if (/^git@(hf\.co|huggingface\.co):spaces\/[^/\s]+\/[^/\s]+(?:\.git)?$/i.test(value)) { | |
| return; | |
| } | |
| throw new Error('remote must be a HuggingFace Space git URL.'); | |
| } | |
| function run(command, args, options = {}) { | |
| const result = spawnSync(command, args, { | |
| cwd: ROOT_DIR, | |
| stdio: 'inherit', | |
| env: process.env, | |
| ...options | |
| }); | |
| if (result.error) { | |
| throw result.error; | |
| } | |
| if (result.status !== 0) { | |
| throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); | |
| } | |
| } | |
| function hasCommand(command) { | |
| const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [command], { stdio: 'pipe' }); | |
| return result.status === 0; | |
| } | |
| function assertSourceDir(sourceDir) { | |
| const resolved = path.resolve(sourceDir); | |
| if (!fs.existsSync(resolved)) { | |
| throw new Error(`Source directory does not exist: ${resolved}`); | |
| } | |
| if (!fs.existsSync(path.join(resolved, 'README.md'))) { | |
| throw new Error(`Source directory is missing README.md: ${resolved}`); | |
| } | |
| } | |
| function configureBinaryStorage(sourceDir) { | |
| if (!hasCommand('git-lfs')) { | |
| return; | |
| } | |
| run('git', ['-C', sourceDir, 'lfs', 'install', '--local']); | |
| run('git', ['-C', sourceDir, 'lfs', 'track', '*.png', '*.jpg', '*.jpeg', '*.webp', '*.gif', '*.ico']); | |
| } | |
| function initDeployRepo(sourceDir, branch) { | |
| const gitDir = path.join(sourceDir, '.git'); | |
| fs.rmSync(gitDir, { recursive: true, force: true }); | |
| run('git', ['-C', sourceDir, 'init', '-b', branch]); | |
| run('git', ['-C', sourceDir, 'config', 'user.name', 'Codex']); | |
| run('git', ['-C', sourceDir, 'config', 'user.email', 'codex@openai.com']); | |
| configureBinaryStorage(sourceDir); | |
| run('git', ['-C', sourceDir, 'add', '-A']); | |
| run('git', ['-C', sourceDir, 'commit', '-m', 'deploy: CodexMobile Relay']); | |
| } | |
| function deployRemote(sourceDir, remote, branch) { | |
| const remoteName = 'huggingface'; | |
| assertHuggingFaceSpaceRemote(remote); | |
| run('git', ['-C', sourceDir, 'remote', 'add', remoteName, remote]); | |
| run('git', ['-C', sourceDir, 'push', '--force', remoteName, `${branch}:${branch}`]); | |
| } | |
| async function main() { | |
| const options = parseArgs(process.argv.slice(2)); | |
| if (!options.remote) { | |
| throw new Error('Missing --remote. Provide the HuggingFace Space git remote URL.'); | |
| } | |
| options.remote = normalizeRemote(options.remote); | |
| assertHuggingFaceSpaceRemote(options.remote); | |
| if (!options.skipPrepare) { | |
| run('npm', ['run', 'space:prepare']); | |
| } | |
| assertDeploySourceReady(options.sourceDir, options.unsafeSource); | |
| initDeployRepo(options.sourceDir, options.branch); | |
| deployRemote(options.sourceDir, options.remote, options.branch); | |
| fs.rmSync(path.join(options.sourceDir, '.git'), { recursive: true, force: true }); | |
| console.log(`Deployed ${options.sourceDir} to ${options.remote} on branch ${options.branch}`); | |
| } | |
| if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { | |
| main().catch((error) => { | |
| console.error(error.message || error); | |
| process.exit(1); | |
| }); | |
| } | |