Files
shannon/apps/cli/src/paths.ts
T
ezl-keygraph 2786f9aa2d feat: surface startup and preflight failures (#454)
* fix: surface pre-workflow worker failures instead of dying silently

* feat: clearer, aggregated config rule validation errors

* fix: preserve blank lines when printing startup errors

* feat: hold start until preflight passes and surface its failure

* fix: cleaner formatting for scan-start failure messages
2026-09-09 02:15:44 +05:30

147 lines
4.3 KiB
TypeScript

/**
* Path resolution for --repo, --config and --models-config arguments.
*
* All three are filesystem paths, absolute or relative to CWD.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fail } from './errors.js';
/**
* Expand a leading `~` or `~/` to the home directory. The shell skips this in the
* `--flag=~/x` form (the tilde is not at the word start), so it must be done here.
*/
export function expandHome(inputPath: string): string {
if (inputPath === '~') {
return os.homedir();
}
if (inputPath.startsWith('~/')) {
return path.join(os.homedir(), inputPath.slice(2));
}
return inputPath;
}
export interface MountPair {
hostPath: string;
containerPath: string;
}
/**
* Hidden subdirectory inside each run directory that holds all internals
* (deliverables, logs, prompts, session state, browser artifacts). Keeps the
* run folder's top level clean so only the final report is visible. Must match
* INTERNAL_DIR in the worker package.
*/
export const INTERNAL_DIR = '.shannon';
/**
* Filename of the human-facing PDF report surfaced at the run directory root.
* Must match FINAL_REPORT_PDF_FILENAME in the worker package.
*/
export const FINAL_REPORT_PDF_FILENAME = 'Security-Assessment-Report.pdf';
/**
* Customer-facing Markdown report name at the run root.
* Must match FINAL_REPORT_MD_FILENAME in the worker package.
*/
export const FINAL_REPORT_MD_FILENAME = 'Security-Assessment-Report.md';
/**
* Reason for a pre-workflow failure, written by the worker under INTERNAL_DIR. The CLI reads it
* during the startup poll to report the real cause instead of a generic timeout. Must match
* STARTUP_ERROR_FILENAME in the worker package.
*/
export const STARTUP_ERROR_FILENAME = 'startup-error.json';
/**
* Resolve a run-directory file (e.g. session.json, workflow.log), preferring the
* current INTERNAL_DIR location and falling back to the legacy run-root location
* so pre-restructure workspaces keep working. Returns the INTERNAL_DIR path when
* neither exists — the right default for new runs and error messages.
*/
export function resolveRunFile(runDir: string, filename: string): string {
const current = path.join(runDir, INTERNAL_DIR, filename);
if (fs.existsSync(current)) {
return current;
}
const legacy = path.join(runDir, filename);
if (fs.existsSync(legacy)) {
return legacy;
}
return current;
}
/**
* Resolve --repo to an absolute path and container mount. The argument is a
* filesystem path, absolute or relative to CWD.
*/
export function resolveRepo(repoArg: string): MountPair {
const hostPath = path.resolve(expandHome(repoArg));
if (!fs.existsSync(hostPath)) {
fail(`Repository not found: ${hostPath}`);
}
if (!fs.statSync(hostPath).isDirectory()) {
fail(`Not a directory: ${hostPath}`);
}
const basename = path.basename(hostPath);
return {
hostPath,
containerPath: `/repos/${basename}`,
};
}
/**
* Resolve --config to absolute path and container mount.
*/
export function resolveConfig(configArg: string): MountPair {
const hostPath = path.resolve(expandHome(configArg));
if (!fs.existsSync(hostPath)) {
fail(`Config file not found: ${hostPath}`);
}
if (!fs.statSync(hostPath).isFile()) {
fail(`Not a file: ${hostPath}`);
}
const basename = path.basename(hostPath);
return {
hostPath,
containerPath: `/app/configs/${basename}`,
};
}
/**
* Container path for a mounted pi model config. Fixed, not derived from the host filename:
* the worker detects the file here to decide whether models.json is enabled at all. Must
* match MODELS_CONFIG_PATH in the worker package.
*/
export const MODELS_CONFIG_CONTAINER_PATH = '/app/models.json';
/**
* Resolve --models-config to an absolute path and container mount. Content is left
* unparsed: pi's models.json permits comments, so JSON.parse would reject valid input,
* and pi's own loader reports schema faults far better — the worker surfaces those.
*/
export function resolveModelsConfig(modelsConfigArg: string): MountPair {
const hostPath = path.resolve(expandHome(modelsConfigArg));
if (!fs.existsSync(hostPath)) {
fail(`Model config file not found: ${hostPath}`);
}
if (!fs.statSync(hostPath).isFile()) {
fail(`Not a file: ${hostPath}`);
}
return {
hostPath,
containerPath: MODELS_CONFIG_CONTAINER_PATH,
};
}