diff --git a/setup/auto.ts b/setup/auto.ts index 94ffe20c5..d4cc873d0 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -122,39 +122,6 @@ async function main(): Promise { } } - // Detect existing .env and offer to reuse it so the user doesn't have to - // paste credentials again on a re-run. - const existingEnv = detectExistingEnv(); - if (existingEnv) { - const lines = Object.values(existingEnv.groups).map( - (g) => ` ${k.green('✓')} ${g.label}`, - ); - note(lines.join('\n'), 'Found existing configuration'); - - const reuseChoice = ensureAnswer( - await brightSelect({ - message: 'Use this existing environment?', - options: [ - { value: 'reuse', label: 'Yes, use what I already have', hint: 'recommended' }, - { value: 'fresh', label: 'No, start fresh' }, - ], - initialValue: 'reuse', - }), - ) as 'reuse' | 'fresh'; - setupLog.userInput('existing_env_choice', reuseChoice); - - if (reuseChoice === 'reuse') { - for (const [key, value] of Object.entries(existingEnv.raw)) { - if (!process.env[key]) process.env[key] = value; - } - if (existingEnv.groups.onecli) skip.add('onecli'); - if (detectRegisteredGroups(process.cwd())) { - skip.add('cli-agent'); - skip.add('first-chat'); - } - } - } - if (!skip.has('container')) { p.log.message(brandBody(dimWrap('Your assistant lives in its own sandbox. It can only see what you explicitly share.', 4))); p.log.message( @@ -344,6 +311,11 @@ async function main(): Promise { return displayName; } + if (!skip.has('cli-agent') && detectRegisteredGroups(process.cwd())) { + skip.add('cli-agent'); + skip.add('first-chat'); + } + if (!skip.has('cli-agent')) { await resolveDisplayName(); const res = await runQuietStep( @@ -1061,56 +1033,6 @@ async function askChannelChoice(): Promise { // ─── interactive / env helpers ───────────────────────────────────────── -interface ExistingEnvGroup { - label: string; - keys: string[]; -} - -const ENV_KEY_GROUPS: Record = { - onecli: { label: 'OneCLI', keys: ['ONECLI_URL'] }, - telegram: { label: 'Telegram', keys: ['TELEGRAM_BOT_TOKEN'] }, - discord: { label: 'Discord', keys: ['DISCORD_BOT_TOKEN', 'DISCORD_APPLICATION_ID', 'DISCORD_PUBLIC_KEY'] }, - slack: { label: 'Slack', keys: ['SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET'] }, - signal: { label: 'Signal', keys: ['SIGNAL_ACCOUNT'] }, - teams: { label: 'Teams', keys: ['TEAMS_APP_ID', 'TEAMS_APP_PASSWORD', 'TEAMS_APP_TENANT_ID', 'TEAMS_APP_TYPE'] }, - whatsapp: { label: 'WhatsApp', keys: ['ASSISTANT_HAS_OWN_NUMBER'] }, - imessage: { label: 'iMessage', keys: ['IMESSAGE_LOCAL', 'IMESSAGE_ENABLED', 'IMESSAGE_SERVER_URL', 'IMESSAGE_API_KEY'] }, -}; - -function detectExistingEnv(): { groups: Record; raw: Record } | null { - const envPath = path.join(process.cwd(), '.env'); - if (!fs.existsSync(envPath)) return null; - - let content: string; - try { - content = fs.readFileSync(envPath, 'utf-8'); - } catch { - return null; - } - - const raw: Record = {}; - for (const line of content.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eq = trimmed.indexOf('='); - if (eq < 1) continue; - raw[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); - } - - if (Object.keys(raw).length === 0) return null; - - const groups: Record = {}; - for (const [id, def] of Object.entries(ENV_KEY_GROUPS)) { - const found = def.keys.filter((key) => raw[key] !== undefined); - if (found.length > 0) { - groups[id] = { label: def.label, keys: found }; - } - } - - if (Object.keys(groups).length === 0) return null; - return { groups, raw }; -} - function anthropicSecretExists(): boolean { try { const res = spawnSync('onecli', ['secrets', 'list'], { diff --git a/setup/channels/discord.ts b/setup/channels/discord.ts index 435956f8f..28c0254d6 100644 --- a/setup/channels/discord.ts +++ b/setup/channels/discord.ts @@ -31,6 +31,7 @@ import { brightSelect } from '../lib/bright-select.js'; import { confirmThenOpen, formatNoteLink } from '../lib/browser.js'; import { askOperatorRole } from '../lib/role-prompt.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; +import { readEnvKey } from '../environment.js'; import { accentGreen, brandBody, fmtDuration, note } from '../lib/theme.js'; const DEFAULT_AGENT_NAME = 'Nano'; @@ -238,7 +239,7 @@ async function walkThroughServerCreation(): Promise { } async function collectDiscordToken(): Promise { - const existing = process.env.DISCORD_BOT_TOKEN?.trim(); + const existing = readEnvKey('DISCORD_BOT_TOKEN'); if (existing && /^[A-Za-z0-9._-]{50,}$/.test(existing)) { const reuse = ensureAnswer(await p.confirm({ message: `Found an existing Discord bot token (${existing.slice(0, 10)}…). Use it?`, diff --git a/setup/channels/imessage.ts b/setup/channels/imessage.ts index a2654c083..8c0b78d14 100644 --- a/setup/channels/imessage.ts +++ b/setup/channels/imessage.ts @@ -37,6 +37,7 @@ import { brightSelect } from '../lib/bright-select.js'; import { askOperatorRole } from '../lib/role-prompt.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { accentGreen, note, wrapForGutter } from '../lib/theme.js'; +import { readEnvKey } from '../environment.js'; const DEFAULT_AGENT_NAME = 'Nano'; @@ -222,8 +223,8 @@ async function walkThroughFullDiskAccess(): Promise { } async function collectRemoteCreds(): Promise { - const existingUrl = process.env.IMESSAGE_SERVER_URL?.trim(); - const existingKey = process.env.IMESSAGE_API_KEY?.trim(); + const existingUrl = readEnvKey('IMESSAGE_SERVER_URL'); + const existingKey = readEnvKey('IMESSAGE_API_KEY'); if (existingUrl && existingKey && /^https?:\/\//i.test(existingUrl)) { const reuse = ensureAnswer(await p.confirm({ message: `Found existing Photon credentials (${existingUrl}). Use them?`, diff --git a/setup/channels/slack.ts b/setup/channels/slack.ts index 24a10ce8d..0e3f05201 100644 --- a/setup/channels/slack.ts +++ b/setup/channels/slack.ts @@ -28,6 +28,7 @@ import * as setupLog from '../logs.js'; import { confirmThenOpen, formatNoteLink } from '../lib/browser.js'; import { askOperatorRole } from '../lib/role-prompt.js'; import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; +import { readEnvKey } from '../environment.js'; import { accentGreen, fmtDuration, note, wrapForGutter } from '../lib/theme.js'; const SLACK_API = 'https://slack.com/api'; @@ -150,7 +151,7 @@ async function walkThroughAppCreation(): Promise { } async function collectBotToken(): Promise { - const existing = process.env.SLACK_BOT_TOKEN?.trim(); + const existing = readEnvKey('SLACK_BOT_TOKEN'); if (existing && existing.startsWith('xoxb-') && existing.length >= 24) { const reuse = ensureAnswer(await p.confirm({ message: `Found an existing Slack bot token (${existing.slice(0, 10)}…). Use it?`, @@ -184,7 +185,7 @@ async function collectBotToken(): Promise { } async function collectSigningSecret(): Promise { - const existing = process.env.SLACK_SIGNING_SECRET?.trim(); + const existing = readEnvKey('SLACK_SIGNING_SECRET'); if (existing && /^[a-f0-9]{16,}$/i.test(existing)) { const reuse = ensureAnswer(await p.confirm({ message: 'Found an existing Slack signing secret. Use it?', diff --git a/setup/channels/teams.ts b/setup/channels/teams.ts index 01839c40d..41e20709a 100644 --- a/setup/channels/teams.ts +++ b/setup/channels/teams.ts @@ -42,6 +42,7 @@ import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js'; import { buildTeamsAppPackage } from '../lib/teams-manifest.js'; import { note } from '../lib/theme.js'; import * as setupLog from '../logs.js'; +import { readEnvKey } from '../environment.js'; const CHANNEL = 'teams'; const MANIFEST_DIR = path.join(process.cwd(), 'data', 'teams'); @@ -60,8 +61,8 @@ export async function runTeamsChannel(_displayName: string): Promise { const collected: Collected = {}; const completed: string[] = []; - const existingAppId = process.env.TEAMS_APP_ID?.trim(); - const existingPassword = process.env.TEAMS_APP_PASSWORD?.trim(); + const existingAppId = readEnvKey('TEAMS_APP_ID'); + const existingPassword = readEnvKey('TEAMS_APP_PASSWORD'); if (existingAppId && existingPassword) { const reuse = ensureAnswer(await p.confirm({ message: `Found existing Teams credentials (App ID: ${existingAppId.slice(0, 8)}…). Use them?`, @@ -70,9 +71,9 @@ export async function runTeamsChannel(_displayName: string): Promise { if (reuse) { collected.appId = existingAppId; collected.appPassword = existingPassword; - collected.appType = (process.env.TEAMS_APP_TYPE?.trim() as 'SingleTenant' | 'MultiTenant') || 'MultiTenant'; + collected.appType = (readEnvKey('TEAMS_APP_TYPE') as 'SingleTenant' | 'MultiTenant') || 'MultiTenant'; if (collected.appType === 'SingleTenant') { - collected.tenantId = process.env.TEAMS_APP_TENANT_ID?.trim(); + collected.tenantId = readEnvKey('TEAMS_APP_TENANT_ID') ?? undefined; } setupLog.userInput('teams_credentials', 'reused-existing'); await installAdapter(collected); diff --git a/setup/channels/telegram.ts b/setup/channels/telegram.ts index 799a97f1b..41ee407a5 100644 --- a/setup/channels/telegram.ts +++ b/setup/channels/telegram.ts @@ -33,6 +33,7 @@ import { spawnStep, writeStepEntry, } from '../lib/runner.js'; +import { readEnvKey } from '../environment.js'; import { accentGreen, brandBold, fitToWidth, fmtDuration, note } from '../lib/theme.js'; const DEFAULT_AGENT_NAME = 'Nano'; @@ -131,7 +132,7 @@ export async function runTelegramChannel(displayName: string): Promise { } async function collectTelegramToken(): Promise { - const existing = process.env.TELEGRAM_BOT_TOKEN?.trim(); + const existing = readEnvKey('TELEGRAM_BOT_TOKEN'); if (existing && /^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(existing)) { const reuse = ensureAnswer(await p.confirm({ message: `Found an existing Telegram bot token (${existing.slice(0, 8)}…). Use it?`, diff --git a/setup/environment.ts b/setup/environment.ts index c351023ab..5960b0ee7 100644 --- a/setup/environment.ts +++ b/setup/environment.ts @@ -11,6 +11,30 @@ import { log } from '../src/log.js'; import { commandExists, getPlatform, isHeadless, isWSL } from './platform.js'; import { emitStatus } from './status.js'; +/** + * Read a single key from `.env` on disk (not process.env). + * Returns the trimmed value or null if the key isn't set / file doesn't exist. + */ +export function readEnvKey(key: string, projectRoot?: string): string | null { + const envPath = path.join(projectRoot ?? process.cwd(), '.env'); + let content: string; + try { + content = fs.readFileSync(envPath, 'utf-8'); + } catch { + return null; + } + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq < 1) continue; + if (trimmed.slice(0, eq) === key) { + return trimmed.slice(eq + 1).trim() || null; + } + } + return null; +} + export function detectExistingDisplayName(projectRoot: string): string | null { const dbPath = path.join(projectRoot, 'data', 'v2.db'); if (!fs.existsSync(dbPath)) return null;