Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
138 lines
5.2 KiB
JavaScript
138 lines
5.2 KiB
JavaScript
import { execFile } from 'node:child_process'
|
||
import { lstat, readlink, realpath } from 'node:fs/promises'
|
||
import { dirname, isAbsolute, join, parse, relative, sep } from 'node:path'
|
||
import { promisify } from 'node:util'
|
||
|
||
const execute = promisify(execFile)
|
||
|
||
export class GitWriteScopeError extends Error {
|
||
constructor(code, message) {
|
||
super(message)
|
||
this.name = 'GitWriteScopeError'
|
||
this.code = code
|
||
}
|
||
}
|
||
|
||
export async function canonicalWriteTarget(absolutePath, signal) {
|
||
if (!isAbsolute(absolutePath)) throw new GitWriteScopeError('invalid-write-path', 'Git scope checks require an absolute file path.')
|
||
let current = parse(absolutePath).root
|
||
let components = absolutePath.slice(current.length).split(sep)
|
||
let followedLinks = 0
|
||
|
||
while (components.length) {
|
||
signal?.throwIfAborted()
|
||
const component = components.shift()
|
||
if (component === '' || component === '.') continue
|
||
if (component === '..') {
|
||
current = dirname(current)
|
||
continue
|
||
}
|
||
|
||
const candidate = join(current, component)
|
||
let info
|
||
try {
|
||
info = await lstat(candidate)
|
||
} catch (error) {
|
||
if (error.code !== 'ENOENT') throw error
|
||
current = candidate
|
||
continue
|
||
}
|
||
|
||
if (info.isSymbolicLink()) {
|
||
if (++followedLinks > 40) throw new GitWriteScopeError('symlink-loop', 'Cannot determine Git scope through a symlink loop.')
|
||
const link = await readlink(candidate)
|
||
const root = parse(link).root
|
||
if (root) current = root
|
||
components = [...link.slice(root.length).split(sep), ...components]
|
||
} else {
|
||
current = candidate
|
||
}
|
||
}
|
||
signal?.throwIfAborted()
|
||
return current
|
||
}
|
||
|
||
async function existingParentDirectory(filePath, signal) {
|
||
let directory = dirname(filePath)
|
||
while (true) {
|
||
signal?.throwIfAborted()
|
||
try {
|
||
const info = await lstat(directory)
|
||
if (!info.isDirectory()) throw new GitWriteScopeError('invalid-write-parent', 'The write target has a non-directory parent.')
|
||
return directory
|
||
} catch (error) {
|
||
if (error.code !== 'ENOENT') throw error
|
||
const parent = dirname(directory)
|
||
if (parent === directory) throw error
|
||
directory = parent
|
||
}
|
||
}
|
||
}
|
||
|
||
function gitEnvironment() {
|
||
return { ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_'))), LC_ALL: 'C' }
|
||
}
|
||
|
||
async function rejectUnresolvedGitMetadata(directory, signal) {
|
||
let current = directory
|
||
while (true) {
|
||
signal?.throwIfAborted()
|
||
try {
|
||
await lstat(join(current, '.git'))
|
||
throw new GitWriteScopeError('git-scope-unavailable', 'Git metadata exists but its repository could not be resolved. Fix it before writing.')
|
||
} catch (error) {
|
||
if (error.code !== 'ENOENT') throw error
|
||
}
|
||
const parent = dirname(current)
|
||
if (parent === current) return
|
||
current = parent
|
||
}
|
||
}
|
||
|
||
export async function repositoryForWriteTarget(filePath, signal) {
|
||
const canonicalPath = await canonicalWriteTarget(filePath, signal)
|
||
const directory = await existingParentDirectory(canonicalPath, signal)
|
||
let result
|
||
|
||
try {
|
||
result = await execute('git', ['-C', directory, 'rev-parse', '--show-toplevel'], {
|
||
encoding: 'utf8',
|
||
env: gitEnvironment(),
|
||
timeout: 10000,
|
||
maxBuffer: 1024 * 1024,
|
||
signal,
|
||
})
|
||
} catch (error) {
|
||
signal?.throwIfAborted()
|
||
if (error.code === 128 && /^fatal: not a git repository \(or any /.test(error.stderr || '')) {
|
||
await rejectUnresolvedGitMetadata(directory, signal)
|
||
return { filePath: canonicalPath, repository: null }
|
||
}
|
||
throw new GitWriteScopeError('git-scope-unavailable', 'Write refused because Git scope could not be verified.')
|
||
}
|
||
|
||
signal?.throwIfAborted()
|
||
const repository = await realpath(result.stdout.replace(/\r?\n$/, ''))
|
||
const relativePath = relative(repository, canonicalPath)
|
||
if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(relativePath)) {
|
||
throw new GitWriteScopeError('git-scope-mismatch', 'Git returned a repository that does not contain the physical write target.')
|
||
}
|
||
return { filePath: canonicalPath, repository }
|
||
}
|
||
|
||
export async function checkGitWriteScope({ filePath, sessionId, store, signal }) {
|
||
signal?.throwIfAborted()
|
||
if (!sessionId) throw new GitWriteScopeError('missing-write-session', 'An active session is required to check the write scope.')
|
||
const target = await repositoryForWriteTarget(filePath, signal)
|
||
|
||
if (!target.repository) {
|
||
return { ...target, warning: `WARNING: ${target.filePath} is outside a Git repository. This write is not protected by Git history. Consider creating a Git repository for this directory. If this path should belong to an existing repository, verify the path and call add_touched_git_directory for that exact repository or worktree.` }
|
||
}
|
||
|
||
const entries = await store.list(sessionId)
|
||
signal?.throwIfAborted()
|
||
if (!entries.some(entry => entry.ok && entry.repository === target.repository)) {
|
||
throw new GitWriteScopeError('unregistered-git-write', `Write blocked: ${target.repository} is not registered in this session’s Git Working Dirs menu. Call add_touched_git_directory for this repository or worktree before writing.`)
|
||
}
|
||
return { ...target, warning: null }
|
||
}
|