mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Packaged diagnostics bundles never contained the daemon or web
`latest.log` — the very logs that hold the agent/critique run flow — so
support exports could not explain "sent prompt to the agent, then
nothing happened" reports.
Root cause: the sidecar `base` means different things per launch path.
tools-dev passes the pre-namespace source root, so
`resolveNamespaceRoot(base, namespace)` is correct. But the packaged
orchestrator launches every child with `base = <namespaceRoot>/runtime`
(apps/packaged/src/{paths,sidecars}.ts) while logs live a level up at
`<namespaceRoot>/logs`. The diagnostics builders re-appended the
namespace and resolved every log to
`<namespaceRoot>/runtime/<namespace>/logs/...` → ENOENT. renderer.log
only survived by accident: the desktop main process wrote it to the
same wrong path the reader looked in.
Add `resolveRuntimeNamespaceRoot(runtime, contract, runtimeMode)` to
`@open-design/sidecar` which walks up out of the `runtime/` dir in
packaged (runtime-mode) launches and falls back to the dev layout
otherwise. Route the desktop renderer-log path and both diagnostics
exporters (desktop IPC + daemon HTTP) through it so writer and reader
stay in lockstep and renderer.log lands next to the desktop log dir.
Tests: sidecar unit specs for both layouts; a daemon export spec that
writes a real `<namespaceRoot>/logs/daemon/latest.log` and asserts the
bundle captures its contents (red on main → ENOENT placeholder, green
here).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5 KiB
TypeScript
142 lines
5 KiB
TypeScript
import { homedir, userInfo } from 'node:os';
|
|
import { dirname } from 'node:path';
|
|
|
|
import type { RequestHandler } from 'express';
|
|
|
|
import {
|
|
buildDiagnosticsZip,
|
|
DIAGNOSTICS_CONTENT_TYPE,
|
|
DIAGNOSTICS_FILENAME_PREFIX,
|
|
diagnosticsFileName,
|
|
type LogSource,
|
|
} from '@open-design/diagnostics';
|
|
import {
|
|
APP_KEYS,
|
|
OPEN_DESIGN_SIDECAR_CONTRACT,
|
|
SIDECAR_MODES,
|
|
type SidecarStamp,
|
|
} from '@open-design/sidecar-proto';
|
|
import {
|
|
resolveLogFilePath,
|
|
resolveRuntimeNamespaceRoot,
|
|
type SidecarRuntimeContext,
|
|
} from '@open-design/sidecar';
|
|
|
|
import { readCurrentAppVersionInfo } from './app-version.js';
|
|
|
|
export interface DiagnosticsHandlerOptions {
|
|
/** Sidecar runtime context, present when daemon is launched via tools-dev or packaged sidecar. */
|
|
runtime: SidecarRuntimeContext<SidecarStamp> | null;
|
|
/** Project root used to derive crash-report match strings. */
|
|
projectRoot: string;
|
|
}
|
|
|
|
const TAIL_BYTES_PER_LOG = 4 * 1024 * 1024;
|
|
|
|
function safeUsername(): string | undefined {
|
|
try {
|
|
const info = userInfo();
|
|
return info?.username && info.username.length > 0 ? info.username : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export const STANDALONE_LAUNCH_WARNING =
|
|
"Daemon started without a sidecar runtime (plain `od` / standalone launch); " +
|
|
"file-based logs are not captured. Re-run via `pnpm tools-dev` or the packaged " +
|
|
"desktop app to include daemon/web/desktop log files in the bundle.";
|
|
|
|
function buildSidecarLogSources(runtime: SidecarRuntimeContext<SidecarStamp> | null): LogSource[] {
|
|
if (runtime == null) return [];
|
|
// In packaged builds `runtime.base` is `<namespaceRoot>/runtime`, so the log
|
|
// tree lives a level UP at `<namespaceRoot>/logs`; `resolveRuntimeNamespaceRoot`
|
|
// accounts for that (a plain `resolveNamespaceRoot` here resolved every
|
|
// daemon/web log to an ENOENT phantom path and captured none of them).
|
|
const namespaceRoot = resolveRuntimeNamespaceRoot({
|
|
contract: OPEN_DESIGN_SIDECAR_CONTRACT,
|
|
runtime,
|
|
runtimeMode: SIDECAR_MODES.RUNTIME,
|
|
});
|
|
const apps = [APP_KEYS.DAEMON, APP_KEYS.WEB, APP_KEYS.DESKTOP];
|
|
const sources: LogSource[] = [];
|
|
for (const app of apps) {
|
|
const absolutePath = resolveLogFilePath({
|
|
app,
|
|
contract: OPEN_DESIGN_SIDECAR_CONTRACT,
|
|
runtimeRoot: namespaceRoot,
|
|
});
|
|
sources.push({
|
|
name: `logs/${app}/latest.log`,
|
|
absolutePath,
|
|
kind: 'text',
|
|
tailBytes: TAIL_BYTES_PER_LOG,
|
|
});
|
|
// Only desktop runs an Electron renderer that writes `renderer.log`
|
|
// (see apps/desktop/src/main/runtime.ts). daemon and web are pure Node
|
|
// services with no renderer process, so listing the file there only
|
|
// produces missing-file placeholders and manifest warnings.
|
|
if (app === APP_KEYS.DESKTOP) {
|
|
sources.push({
|
|
name: `logs/${app}/renderer.log`,
|
|
absolutePath: `${dirname(absolutePath)}/renderer.log`,
|
|
kind: 'text',
|
|
tailBytes: TAIL_BYTES_PER_LOG,
|
|
});
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
export function createDiagnosticsExportHandler(options: DiagnosticsHandlerOptions): RequestHandler {
|
|
return async (_req, res) => {
|
|
try {
|
|
const versionInfo = await readCurrentAppVersionInfo().catch(() => null);
|
|
const sources = buildSidecarLogSources(options.runtime);
|
|
const username = safeUsername();
|
|
const home = homedir();
|
|
|
|
const result = await buildDiagnosticsZip({
|
|
context: {
|
|
app: {
|
|
name: 'open-design',
|
|
version: versionInfo?.version,
|
|
channel: versionInfo?.channel,
|
|
packaged: versionInfo?.packaged,
|
|
},
|
|
source: 'daemon-http',
|
|
namespace: options.runtime?.namespace,
|
|
extra: {
|
|
runtimeAvailable: options.runtime != null,
|
|
sourceTag: options.runtime?.source ?? null,
|
|
mode: options.runtime?.mode ?? null,
|
|
base: options.runtime?.base ?? null,
|
|
projectRoot: options.projectRoot,
|
|
},
|
|
warnings: options.runtime == null ? [STANDALONE_LAUNCH_WARNING] : undefined,
|
|
},
|
|
sources,
|
|
redaction: { username },
|
|
crashReports: {
|
|
// Restrict to Open Design's own process names. A generic "Electron"
|
|
// substring would sweep up crash reports from any other Electron
|
|
// app on the host (VS Code, Slack, …) and leak unrelated user data
|
|
// into the support bundle.
|
|
matchSubstrings: ['Open Design', 'open-design'],
|
|
withinDays: 7,
|
|
maxReports: 10,
|
|
homeDir: home,
|
|
},
|
|
});
|
|
|
|
const filename = diagnosticsFileName(DIAGNOSTICS_FILENAME_PREFIX);
|
|
res.setHeader('Content-Type', DIAGNOSTICS_CONTENT_TYPE);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
res.status(200).end(result.zip);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
res.status(500).json({ error: 'DIAGNOSTICS_EXPORT_FAILED', message });
|
|
}
|
|
};
|
|
}
|