diff --git a/api/website.ts b/api/website.ts index d382da6..3f3fa06 100644 --- a/api/website.ts +++ b/api/website.ts @@ -1,5 +1,5 @@ import { mkdir, readdir, stat } from 'node:fs/promises'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { getOwnerHomeDir } from '@@/data-path'; import { readSettings } from './settings'; @@ -49,11 +49,55 @@ export type WebsiteStatus = { export type RunResult = { ok: boolean; output: string }; +/** + * Several commands, reported as one. + * + * Each is prefixed with the command line that produced it, so the panel reads like the terminal + * somebody would otherwise have opened — and a clone that worked followed by an install that did not + * is legible as exactly that, rather than as one opaque failure. + */ +const sequence = async (steps: Array<{ label: string; run: () => Promise }>): Promise => { + const parts: string[] = []; + + for (const step of steps) { + parts.push(`$ ${step.label}`); + const result = await step.run(); + if (result.output) parts.push(result.output); + // Stop at the first failure. Installing into a clone that did not happen produces a second, more + // confusing error about a directory that is not there. + if (!result.ok) return { ok: false, output: parts.join('\n') }; + parts.push(''); + } + + return { ok: true, output: parts.join('\n').trim() }; +}; + +/** + * `bun install`, with the bun that is ALREADY RUNNING. + * + * `process.execPath` rather than the string "bun": the server's PATH is whatever PM2 was started with, + * which on more than one host does not include `~/.bun/bin`. Looking bun up by name works in a shell + * and fails inside the process, which is the worst version of this bug — it cannot be reproduced by + * running the same command by hand. + */ +const install = (path: string, email: string): Promise => run([process.execPath, 'install'], path, email); + +/* + The server's own PATH does not include bun. + + Verified on fofos 2026-08-29: PM2 started `officer` with `/usr/local/sbin:…:/snap/bin` and nothing + else, while bun lives in `~/.bun/bin`. Spawning by absolute path is enough for `bun install` itself — + but a postinstall script that calls `bun` would not find it, and that failure reads as a broken + package rather than as a missing PATH entry. +*/ +const BUN_DIR = dirname(process.execPath); + const run = async (argv: string[], cwd: string, email: string): Promise => { const proc = Bun.spawn(argv, { cwd, env: { ...process.env, + PATH: `${BUN_DIR}:${process.env.PATH ?? ''}`, HOME: getOwnerHomeDir(email), // See the note at the top: these two are the difference between a failure and a hang. GIT_TERMINAL_PROMPT: '0', @@ -144,7 +188,13 @@ export async function cloneWebsite(email: string): Promise { // Full history, not `--depth 1`. This is a working checkout somebody will pull into and read the log // of, and a shallow clone turns both into a support question later. - return run(['git', 'clone', '--progress', REMOTE, path], settings.rootPath, email); + return sequence([ + { + label: `git clone ${REMOTE} ${path}`, + run: () => run(['git', 'clone', '--progress', REMOTE, path], settings.rootPath, email), + }, + { label: 'bun install', run: () => install(path, email) }, + ]); } /** @@ -158,5 +208,8 @@ export async function updateWebsite(email: string): Promise { if (!status.isRepo) return { ok: false, output: `${status.path} is not a git checkout.` }; if (status.dirty) return { ok: false, output: 'There are uncommitted changes. Sort those out first.' }; - return run(['git', 'pull', '--ff-only', '--progress'], status.path, email); + return sequence([ + { label: 'git pull --ff-only', run: () => run(['git', 'pull', '--ff-only', '--progress'], status.path, email) }, + { label: 'bun install', run: () => install(status.path, email) }, + ]); } diff --git a/manifest.ts b/manifest.ts index b529628..147a071 100644 --- a/manifest.ts +++ b/manifest.ts @@ -35,7 +35,7 @@ import type { PluginManifest } from '@@/plugins/manifest'; */ export const manifest: PluginManifest = { publisher: 'officerdev', - version: '2026.08.29.10', + version: '2026.08.29.11', platform: '>=1.0.0', label: 'Fofos',