import fs from "node:fs"; import path from "node:path"; import { z } from "zod"; import { readJsonFile } from "../lib/parse-json"; const PackageJsonScripts = z.object({ scripts: z.record(z.string(), z.string()).optional(), }); export interface ScriptEntry { name: string; label: string; cwd: string; } export function discoverApps(root: string): string[] { const appsDir = path.join(root, "apps"); if (!fs.existsSync(appsDir)) return []; return fs .readdirSync(appsDir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name) .sort(); } function readScripts(pkgPath: string): Record { if (!fs.existsSync(pkgPath)) return {}; const result = readJsonFile(pkgPath, PackageJsonScripts); if (!result.ok) { // A malformed package.json should not abort script discovery for the rest of the app console.error(`Warning: skipping ${pkgPath}: ${result.error}`); return {}; } return result.data.scripts ?? {}; } export function collectScripts(root: string, app: string): ScriptEntry[] { const appDir = path.join(root, "apps", app); const entries: ScriptEntry[] = []; // Root package.json const rootScripts = readScripts(path.join(appDir, "package.json")); for (const name of Object.keys(rootScripts)) { entries.push({ name, label: name, cwd: appDir }); } // One-level subdirectory package.json files if (!fs.existsSync(appDir)) return entries; for (const dirent of fs.readdirSync(appDir, { withFileTypes: true })) { if (!dirent.isDirectory() || dirent.name === "node_modules") continue; const subDir = path.join(appDir, dirent.name); const scripts = readScripts(path.join(subDir, "package.json")); for (const name of Object.keys(scripts)) { entries.push({ name, label: `${dirent.name} > ${name}`, cwd: subDir }); } } return entries; }