clone and update both end in bun install
a checkout with no node_modules does not run, so cloning without installing left something that looked finished and was not. run after an update too, and UNCONDITIONALLY — including when the pull changed nothing. skipping it on "already up to date" is the shortcut that is right until the one time it is not, and an install on an unchanged tree is close to free. `process.execPath`, NEVER the string "bun". verified on fofos today: PM2 started officer with PATH=/usr/local/sbin:…:/snap/bin and bun lives in ~/.bun/bin, so looking it up by name fails inside the process and works perfectly when you run the same command by hand — the worst version of this bug, because it cannot be reproduced the obvious way. execPath is the bun already running us. bun's directory is also prepended to PATH for the child, which execPath alone does not cover: a postinstall script calling `bun` would not find it, and that reads as a broken package rather than a missing PATH entry. the two commands report as ONE, each prefixed with the line that produced it, so the panel reads like the terminal somebody would otherwise have opened. it stops 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 — and a clone that worked followed by an install that did not is legible as exactly that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+56
-3
@@ -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<RunResult> }>): Promise<RunResult> => {
|
||||
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<RunResult> => 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<RunResult> => {
|
||||
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<RunResult> {
|
||||
|
||||
// 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<RunResult> {
|
||||
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) },
|
||||
]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user