/**
* Userland (same-account) SSH push setup — classic-git-auth Phase 2.
*
* In the same-account model the daemon runs as the operator's user and manages
* a marked block in that user's OWN ~/.ssh/authorized_keys. The forced command
* points at a tiny launcher in ~/.local/bin that the daemon self-installs here,
* so there is no separate `git` account, no root, and no package-bin plumbing —
* the wrapper runs as the daemon's user and reaches the runtime, socket, and
* repos natively. See spec/flows/classic-git-auth.md.
*/
import fs from '#node:fs/promises'
import { dirname } from '#node:path'
/**
* Idempotently install the pgv3-git-shell launcher: a `sh` shim that execs the
* bare/node runtime on the wrapper script. Returns the launcher path to bake
* into the authorized_keys forced command.
*
* @param {string} launcherPath - e.g. ~/.local/bin/pgv3-git-shell
* @param {object} opts - { runtime, script } (absolute paths)
* @returns {Promise<{ launcherPath, runtime, script }>}
*/
export async function ensureGitShellLauncher (launcherPath, opts = {}) {
const { runtime, script } = opts
if (!launcherPath || !runtime || !script) {
throw new Error('ensureGitShellLauncher requires launcherPath, runtime, script')
}
for (const v of [runtime, script]) {
if (typeof v !== 'string' || /[\r\n"]/.test(v)) throw new Error('invalid runtime/script path')
}
const body =
'#!/bin/sh\n' +
'# managed-by=pear-git-v3 — pgv3-git-shell launcher (do not edit)\n' +
'exec "' + runtime + '" "' + script + '" "$@"\n'
await fs.mkdir(dirname(launcherPath), { recursive: true }).catch(() => {})
// Only rewrite when changed (a redeploy moves the runtime/script paths).
let current = null
try { current = await fs.readFile(launcherPath, 'utf8') } catch {}
if (current !== body) {
const tmp = launcherPath + '.pgv3-tmp'
await fs.writeFile(tmp, body, { mode: 0o755 })
await fs.rename(tmp, launcherPath)
}
try { await fs.chmod(launcherPath, 0o755) } catch {}
return { launcherPath, runtime, script }
}