diff --git a/package-lock.json b/package-lock.json index 7352330..b37d398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@clack/prompts": "^1.2.0", "@companion-ai/alpha-hub": "^0.1.3", "@mariozechner/pi-ai": "^0.66.1", "@mariozechner/pi-coding-agent": "^0.66.1", @@ -780,6 +781,28 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@clack/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.2.0.tgz", + "integrity": "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.2.0.tgz", + "integrity": "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.2.0", + "fast-string-width": "^1.1.0", + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, "node_modules/@companion-ai/alpha-hub": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@companion-ai/alpha-hub/-/alpha-hub-0.1.3.tgz", @@ -3206,6 +3229,21 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz", + "integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz", + "integrity": "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^1.2.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -3222,6 +3260,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz", + "integrity": "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^1.1.0" + } + }, "node_modules/fast-xml-builder": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", @@ -4611,6 +4658,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index 4894b0f..e2da676 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "module", "engines": { - "node": ">=20.19.0" + "node": ">=20.19.0 <25" }, "bin": { "feynman": "bin/feynman.js" @@ -59,6 +59,7 @@ ] }, "dependencies": { + "@clack/prompts": "^1.2.0", "@companion-ai/alpha-hub": "^0.1.3", "@mariozechner/pi-ai": "^0.66.1", "@mariozechner/pi-coding-agent": "^0.66.1", diff --git a/src/setup/setup.ts b/src/setup/setup.ts index e0f2888..7727946 100644 --- a/src/setup/setup.ts +++ b/src/setup/setup.ts @@ -1,15 +1,24 @@ import { isLoggedIn as isAlphaLoggedIn, login as loginAlpha } from "@companion-ai/alpha-hub/lib"; +import { dirname } from "node:path"; -import { getDefaultSessionDir, getFeynmanHome } from "../config/paths.js"; -import { getPiWebAccessStatus, getPiWebSearchConfigPath } from "../pi/web-access.js"; +import { getPiWebAccessStatus } from "../pi/web-access.js"; import { normalizeFeynmanSettings } from "../pi/settings.js"; import type { ThinkingLevel } from "../pi/settings.js"; +import { getMissingConfiguredPackages, installPackageSources } from "../pi/package-ops.js"; +import { listOptionalPackagePresets } from "../pi/package-presets.js"; import { getCurrentModelSpec, runModelSetup } from "../model/commands.js"; import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js"; import { PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js"; import { setupPreviewDependencies } from "./preview.js"; -import { runDoctor } from "./doctor.js"; import { printInfo, printSection, printSuccess } from "../ui/terminal.js"; +import { + isInteractiveTerminal, + promptConfirm, + promptIntro, + promptMultiSelect, + promptOutro, + SetupCancelledError, +} from "./prompts.js"; type SetupOptions = { settingsPath: string; @@ -21,10 +30,6 @@ type SetupOptions = { defaultThinkingLevel?: ThinkingLevel; }; -function isInteractiveTerminal(): boolean { - return Boolean(process.stdin.isTTY && process.stdout.isTTY); -} - function printNonInteractiveSetupGuidance(): void { printInfo("Non-interactive terminal. Use explicit commands:"); printInfo(" feynman model login "); @@ -34,37 +39,181 @@ function printNonInteractiveSetupGuidance(): void { printInfo(" feynman doctor"); } +function summarizePackageSources(sources: string[]): string { + if (sources.length <= 3) { + return sources.join(", "); + } + + return `${sources.slice(0, 3).join(", ")} +${sources.length - 3} more`; +} + +async function maybeInstallBundledPackages(options: SetupOptions): Promise { + const agentDir = dirname(options.authPath); + const { missing, bundled } = getMissingConfiguredPackages(options.workingDir, agentDir, options.appRoot); + const userMissing = missing.filter((entry) => entry.scope === "user").map((entry) => entry.source); + const projectMissing = missing.filter((entry) => entry.scope === "project").map((entry) => entry.source); + + printSection("Packages"); + if (bundled.length > 0) { + printInfo(`Bundled research packages ready: ${summarizePackageSources(bundled.map((entry) => entry.source))}`); + } + + if (missing.length === 0) { + printInfo("No additional package install required."); + return; + } + + printInfo(`Missing packages: ${summarizePackageSources(missing.map((entry) => entry.source))}`); + const shouldInstall = await promptConfirm("Install missing Feynman packages now?", true); + if (!shouldInstall) { + printInfo("Skipping package install. Feynman may install missing packages later if needed."); + return; + } + + if (userMissing.length > 0) { + try { + await installPackageSources(options.workingDir, agentDir, userMissing); + printSuccess(`Installed bundled packages: ${summarizePackageSources(userMissing)}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + printInfo(message.includes("No supported package manager found") + ? "No package manager available for additional installs. The standalone bundle can still run with its shipped packages." + : `Package install skipped: ${message}`); + } + } + + if (projectMissing.length > 0) { + try { + await installPackageSources(options.workingDir, agentDir, projectMissing, { local: true }); + printSuccess(`Installed project packages: ${summarizePackageSources(projectMissing)}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + printInfo(`Project package install skipped: ${message}`); + } + } +} + +async function maybeInstallOptionalPackages(options: SetupOptions): Promise { + const agentDir = dirname(options.authPath); + const presets = listOptionalPackagePresets(); + if (presets.length === 0) { + return; + } + + const selectedPresets = await promptMultiSelect( + "Optional packages", + presets.map((preset) => ({ + value: preset.name, + label: preset.name, + hint: preset.description, + })), + [], + ); + + if (selectedPresets.length === 0) { + printInfo("No optional packages selected."); + return; + } + + for (const presetName of selectedPresets) { + const preset = presets.find((entry) => entry.name === presetName); + if (!preset) continue; + try { + await installPackageSources(options.workingDir, agentDir, preset.sources, { + persist: true, + }); + printSuccess(`Installed optional preset: ${preset.name}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + printInfo(message.includes("No supported package manager found") + ? `Skipped optional preset ${preset.name}: no package manager available.` + : `Skipped optional preset ${preset.name}: ${message}`); + } + } +} + +async function maybeLoginAlpha(): Promise { + if (isAlphaLoggedIn()) { + printInfo("alphaXiv already configured."); + return; + } + + const shouldLogin = await promptConfirm("Connect alphaXiv now?", true); + if (!shouldLogin) { + printInfo("Skipping alphaXiv login for now."); + return; + } + + try { + await loginAlpha(); + printSuccess("alphaXiv login complete"); + } catch (error) { + printInfo(`alphaXiv login skipped: ${error instanceof Error ? error.message : String(error)}`); + } +} + +async function maybeInstallPreviewDependencies(): Promise { + if (resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS)) { + printInfo("Preview support already configured."); + return; + } + + const shouldInstall = await promptConfirm("Install pandoc for preview/export support?", false); + if (!shouldInstall) { + printInfo("Skipping preview dependency install."); + return; + } + + try { + const result = setupPreviewDependencies(); + printSuccess(result.message); + } catch (error) { + printInfo(`Preview setup skipped: ${error instanceof Error ? error.message : String(error)}`); + } +} + export async function runSetup(options: SetupOptions): Promise { if (!isInteractiveTerminal()) { printNonInteractiveSetupGuidance(); return; } - await runModelSetup(options.settingsPath, options.authPath); + try { + await promptIntro("Feynman setup"); + await runModelSetup(options.settingsPath, options.authPath); + await maybeInstallBundledPackages(options); + await maybeInstallOptionalPackages(options); + await maybeLoginAlpha(); + await maybeInstallPreviewDependencies(); - if (!isAlphaLoggedIn()) { - await loginAlpha(); - printSuccess("alphaXiv login complete"); + normalizeFeynmanSettings( + options.settingsPath, + options.bundledSettingsPath, + options.defaultThinkingLevel ?? "medium", + options.authPath, + ); + + const modelStatus = buildModelStatusSnapshotFromRecords( + getSupportedModelRecords(options.authPath), + getAvailableModelRecords(options.authPath), + getCurrentModelSpec(options.settingsPath), + ); + printSection("Ready"); + printInfo(`Model: ${getCurrentModelSpec(options.settingsPath) ?? "not set"}`); + printInfo(`alphaXiv: ${isAlphaLoggedIn() ? "configured" : "not configured"}`); + printInfo(`Preview: ${resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS) ? "configured" : "not configured"}`); + printInfo(`Web: ${getPiWebAccessStatus().routeLabel}`); + if (modelStatus.recommended && !modelStatus.currentValid) { + printInfo(`Recommended model: ${modelStatus.recommended}`); + } + + await promptOutro("Feynman is ready"); + } catch (error) { + if (error instanceof SetupCancelledError) { + printInfo("Setup cancelled."); + return; + } + + throw error; } - - const result = setupPreviewDependencies(); - printSuccess(result.message); - - normalizeFeynmanSettings( - options.settingsPath, - options.bundledSettingsPath, - options.defaultThinkingLevel ?? "medium", - options.authPath, - ); - - const modelStatus = buildModelStatusSnapshotFromRecords( - getSupportedModelRecords(options.authPath), - getAvailableModelRecords(options.authPath), - getCurrentModelSpec(options.settingsPath), - ); - printSection("Ready"); - printInfo(`Model: ${getCurrentModelSpec(options.settingsPath) ?? "not set"}`); - printInfo(`alphaXiv: ${isAlphaLoggedIn() ? "configured" : "not configured"}`); - printInfo(`Preview: ${resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS) ? "configured" : "not configured"}`); - printInfo(`Web: ${getPiWebAccessStatus().routeLabel}`); }