import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import { password, select } from '@inquirer/prompts';
import { readClaudeAccountEmail, resolveClaudeSetupToken, resolveClaudeSetupTokenForEmail, seedClaudeWorkerHomeIdentity } from '../lib/claude-account-token.js';
import { setHelpSections } from '../lib/help.js';
import { readMeta, updateMeta } from '../lib/state.js';
import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
import { assertNativeAccountNameable, nativeAccountCapability, nativeIdentityKey } from '../lib/account-capabilities.js';
import { collectRunCandidates } from '../lib/accounting/rotate.js';
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
import { buildSwitchAccountChoices, formatAccountLimits, pickSwitchAccount } from './run-account-picker.js';
import { profileExists, readProfile } from '../lib/profiles.js';
import { pushBundleToHost } from '../lib/secrets/push.js';
import { assertCredentialTransportHostPinned, resolveHostSshTarget } from '../lib/secrets/remote.js';
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
import { runDevicesAccounts } from './ssh.js';
import { discoverNativeAccounts } from '../lib/account-catalog.js';
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
import { registerMintCommand } from './auth-mint.js';
function cleanCommandError(command, err) {
    command.error(err instanceof Error ? err.message : String(err), { exitCode: 1, code: 'accounts.error' });
}
/** Turn a thrown user-facing Error into commander's clean CLI error instead of Node's stack dump. */
async function runAccountsAction(command, fn) {
    try {
        await fn();
    }
    catch (err) {
        if (isPromptCancelled(err))
            process.exit(130);
        cleanCommandError(command, err);
    }
}
function parseInstallation(raw) {
    const at = raw.lastIndexOf('@');
    if (at < 1 || at === raw.length - 1)
        throw new Error(`Expected <agent>@<version>, got '${raw}'.`);
    const agent = resolveAgentName(raw.slice(0, at));
    if (!agent)
        throw new Error(`Unknown agent '${raw.slice(0, at)}'.`);
    return { agent, version: raw.slice(at + 1) };
}
export async function nativeIdentityFromSource(raw) {
    const parsed = parseInstallation(raw);
    const capability = nativeAccountCapability(parsed.agent);
    assertNativeAccountNameable(parsed.agent);
    if (!listInstalledVersions(parsed.agent).includes(parsed.version))
        throw new Error(`${raw} is not installed.`);
    const info = await getAccountInfo(parsed.agent, getVersionHomePath(parsed.agent, parsed.version));
    const identityKey = nativeIdentityKey(info, capability);
    if (!identityKey)
        throw new Error(`${raw} has no stable signed-in identity. Run it and complete its normal login first.`);
    return { agent: parsed.agent, version: parsed.version, identityKey, identityLabel: info.email ?? undefined, scope: capability.scope };
}
/**
 * Classify + validate an attach target, rejecting a typo BEFORE any binding is
 * written: it must be an existing custom-harness profile, an installed
 * `agent@version`, or a known harness id.
 */
export function classifyAttachTarget(target) {
    if (profileExists(target))
        return { kind: 'profile', profile: readProfile(target) };
    if (target.includes('@')) {
        const at = target.lastIndexOf('@');
        const agent = resolveAgentName(target.slice(0, at));
        const version = target.slice(at + 1);
        if (!agent)
            throw new Error(`Unknown harness '${target.slice(0, at)}' in target '${target}'.`);
        if (!version)
            throw new Error(`Target '${target}' is missing a version.`);
        if (!listInstalledVersions(agent).includes(version))
            throw new Error(`${agent}@${version} is not installed.`);
        return { kind: 'installation', agent, version };
    }
    const agent = resolveAgentName(target);
    if (agent)
        return { kind: 'device-agent', agent };
    throw new Error(`Unknown attach target '${target}'. Expected an installed <agent>@<version>, a harness id, or an existing custom harness profile.`);
}
/**
 * Persist the attached setup-token to a per-version `.oauth_token` file so an
 * INTERACTIVE Claude launch on a keychain-less Linux worker can authenticate from it.
 * Headless runs inject the token via `buildExecEnv`. Since PHNX-3502 an interactive
 * launch on a worker-role device ALSO injects it through `claudeAdapter.applyExecConfigEnv`
 * (only a headed `personal`/`desktop` device defers to its per-version native login), and
 * the shim's Linux fallback (`claudeAdapter.shimConfigEnvBash`) reads exactly this file —
 * so writing it is what makes a freshly-attached setup-token visible to that shim fallback
 * on a worker. macOS keeps the credential in the keychain, so `resolveClaudeSetupToken`
 * returns null there and this is a no-op off Linux.
 */
export function writeClaudeInteractiveOauthToken(target, targetAgent, email) {
    if (process.platform !== 'linux' || targetAgent !== 'claude' || target.kind !== 'installation')
        return;
    const versionHome = getVersionHomePath('claude', target.version);
    const tokenPath = path.join(versionHome, '.claude', '.oauth_token');
    // Resolve by the attached account's email when known (a freshly-seeded worker
    // home the `.claude.json` read below could not key on yet), else by the home's
    // own recorded identity for a re-point/detach.
    const token = email
        ? resolveClaudeSetupTokenForEmail(email, versionHome)
        : resolveClaudeSetupToken(versionHome);
    // A re-point (attach B over A, or a detach) can leave no setup-token resolving for
    // this version — B's may not be minted yet. A leftover file from the previous binding
    // would silently authenticate interactive runs as the OLD account (the shim's Linux
    // fallback reads it), so clear it rather than leave it stale.
    if (!token) {
        fs.rmSync(tokenPath, { force: true });
        return;
    }
    fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
    fs.writeFileSync(tokenPath, token, { mode: 0o600 });
}
export function parseBundleKey(raw) {
    const colon = raw.indexOf(':');
    if (colon < 1 || colon === raw.length - 1)
        throw new Error(`Expected bundle:key, got '${raw}'.`);
    return { bundle: raw.slice(0, colon), key: raw.slice(colon + 1) };
}
function secretFromBundle(raw) {
    const { bundle, key } = parseBundleKey(raw);
    return readAndResolveBundleEnv(bundle, { keys: [key], keyMode: 'storage', agentOnly: true, caller: 'accounts import' }).env[key];
}
/** Interactive secret entry for add/set-key. Ctrl+C must not become accounts.error. */
async function promptAccountSecret(message) {
    try {
        return await password({ message });
    }
    catch (err) {
        if (isPromptCancelled(err))
            return null;
        throw err;
    }
}
function publicAccount(account) {
    return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent };
}
async function printAccounts(json, fleet = false) {
    if (fleet)
        return runDevicesAccounts({ json });
    const records = Object.values(readAccountRegistry().accounts).sort((a, b) => a.name.localeCompare(b.name));
    const discovered = await discoverNativeAccounts();
    const savedNative = listNativeAccounts(readMeta());
    const native = discovered.map(row => {
        const saved = savedNative.find(account => account.agent === row.agent && account.identityKey === row.id);
        return { ...row, name: saved?.name, id: saved?.id ?? row.id };
    });
    if (json) {
        console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name))), ...native], null, 2));
        return;
    }
    console.log(renderAccountList(records, native));
}
/** Pure text renderer for `agents accounts list` — grouped, column-aligned, labels first-class. */
export function renderAccountList(records, native) {
    const out = [];
    // Native logins, grouped by harness — the selection surface for `<harness>#<label>`.
    out.push(chalk.bold('Native logins') + chalk.gray('     ') + chalk.gray('run <harness>#<label>'));
    if (!native.length) {
        out.push(chalk.gray('  No signed-in native accounts found.'));
    }
    else {
        const byHarness = new Map();
        for (const acct of native) {
            const list = byHarness.get(acct.agent) ?? [];
            list.push(acct);
            byHarness.set(acct.agent, list);
        }
        const harnessW = Math.max(6, ...[...byHarness.keys()].map(h => h.length));
        const labelW = Math.max(6, ...native.map(a => (a.name ?? '—').length));
        const idW = Math.max(8, ...native.map(a => a.display.length));
        let sawDefault = false;
        for (const [harness, list] of [...byHarness.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
            const defaultVersion = getGlobalDefault(harness);
            list.forEach((acct, i) => {
                const harnessCell = chalk.gray((i === 0 ? harness : '').padEnd(harnessW));
                const rawLabel = acct.name ?? '—';
                const labelCell = acct.name ? chalk.cyan(rawLabel.padEnd(labelW)) : chalk.gray(rawLabel.padEnd(labelW));
                const isDefault = defaultVersion ? acct.versions.includes(defaultVersion) : false;
                if (isDefault)
                    sawDefault = true;
                const marker = isDefault ? chalk.green('*') : ' ';
                out.push(`  ${harnessCell}  ${labelCell}${marker} ${acct.display.padEnd(idW)}  ${chalk.gray(acct.versions.join(', '))}`);
            });
        }
        if (sawDefault)
            out.push(chalk.gray('\n  * default account for that harness (used when you pass neither @version nor #label)'));
    }
    // Provider bundles — the durable API-key/setup-token accounts, selected with `--account <name>`.
    out.push('');
    out.push(chalk.bold('Provider bundles') + chalk.gray('  run <harness> --account <name>'));
    if (!records.length) {
        out.push(chalk.gray("  None. Add one with 'agents accounts add <name> --provider <provider> --auth <type>'."));
    }
    else {
        const nameW = Math.max(4, ...records.map(a => a.name.length));
        const provW = Math.max(8, ...records.map(a => a.provider.length));
        const authW = Math.max(10, ...records.map(a => a.auth.length));
        for (const account of records) {
            const present = inspectAccount(account.name).secretPresent ? chalk.green('ready') : chalk.red('missing on this device');
            out.push(`  ${chalk.cyan(account.name.padEnd(nameW))}  ${account.provider.padEnd(provW)}  ${account.auth.padEnd(authW)}  ${present}`);
        }
    }
    return out.join('\n');
}
function parseAuth(raw) {
    if (raw === 'api-key' || raw === 'setup-token' || raw === 'bearer-token')
        return raw;
    throw new Error(`Unsupported auth type '${raw}'. Use api-key, setup-token, or bearer-token.`);
}
function parseHarness(agentRaw) {
    if (!ALL_AGENT_IDS.includes(agentRaw))
        throw new Error(`Unknown agent '${agentRaw}'.`);
    return agentRaw;
}
/**
 * Fold signed-in run candidates into distinct identities — the unit a label
 * binds to. The same account signed into several versions is ONE row, so a
 * multi-version single-account harness never demands a selector. Candidates
 * with no stable identity (no accountKey, no email) are dropped: there is
 * nothing durable for a label to bind to. Default-version identity first.
 */
export function groupLabelIdentities(candidates, defaultVersion) {
    const byKey = new Map();
    for (const candidate of candidates) {
        const key = candidate.accountKey ?? candidate.email?.toLowerCase();
        if (!key)
            continue;
        const row = byKey.get(key) ?? { identityKey: key, email: null, versions: [], isDefault: false };
        row.email ??= candidate.email;
        row.versions.push(candidate.version);
        if (candidate.version === defaultVersion)
            row.isDefault = true;
        byKey.set(key, row);
    }
    return [...byKey.values()].sort((a, b) => a.isDefault !== b.isDefault ? (a.isDefault ? -1 : 1) : (a.email ?? a.identityKey).localeCompare(b.email ?? b.identityKey));
}
/**
 * Resolve which signed-in login a bare-harness `accounts label` call means.
 * `collect` is injectable (the `resolveRunVersion` pattern) so tests drive the
 * real selection path against fixture candidates.
 */
export async function resolveLabelIdentity(agent, accountSelector, collect = collectRunCandidates) {
    const candidates = (await collect(agent)).filter(candidate => candidate.signedIn);
    const identities = groupLabelIdentities(candidates, getGlobalDefault(agent));
    if (identities.length === 0)
        throw new Error(`No signed-in ${agent} account with a stable identity. Run the harness and complete its login first.`);
    const needle = accountSelector?.toLowerCase();
    if (needle) {
        const match = identities.find(identity => identity.email?.toLowerCase() === needle || identity.identityKey.toLowerCase() === needle);
        if (!match)
            throw new Error(`Unknown ${agent} account '${accountSelector}'.`);
        return { kind: 'selected', identity: match };
    }
    if (identities.length === 1)
        return { kind: 'selected', identity: identities[0] };
    return { kind: 'ambiguous', identities };
}
/**
 * Body of `accounts label`, exported with the injectable candidate collector
 * so tests exercise everything but the TTY prompt itself.
 */
export async function runAccountsLabel(source, label, opts, collect = collectRunCandidates) {
    if (source.includes('@')) {
        // <harness>@<version> pins the login by where it is signed in, so a
        // second selector can only contradict it.
        if (opts.account)
            throw new Error(`'${source}' already selects one login; drop --account.`);
        const identity = await nativeIdentityFromSource(source);
        const account = labelNativeAccount(identity.agent, identity.identityKey, identity.identityLabel, label, identity.scope);
        console.log(chalk.green(`Labeled ${identity.agent} account ${identity.identityLabel ?? identity.identityKey} as '${account.name}'.`));
        return;
    }
    const agent = parseHarness(source);
    assertNativeAccountNameable(agent);
    const selection = await resolveLabelIdentity(agent, opts.account, collect);
    let selected;
    if (selection.kind === 'selected') {
        selected = selection.identity;
    }
    else {
        if (!isInteractiveTerminal()) {
            requireInteractiveSelection(`Selecting the ${agent} login to label`, [
                `agents accounts label ${agent} ${label ?? '<label>'} --account <email|id>`,
                `agents accounts label ${agent}@<version> ${label ?? '<label>'}`,
            ]);
        }
        const picked = await pickLabelIdentity(agent, selection.identities);
        if (!picked)
            return;
        selected = picked;
    }
    const account = labelNativeAccount(agent, selected.identityKey, selected.email ?? undefined, label, nativeAccountCapability(agent).scope);
    console.log(chalk.green(`Labeled ${agent} account ${selected.email ?? selected.identityKey} as '${account.name}'.`));
}
/** Prompt for the login a label should bind to. A cancelled picker writes nothing. */
async function pickLabelIdentity(agent, identities) {
    const idW = Math.max(0, ...identities.map(identity => (identity.email ?? identity.identityKey).length));
    const choices = identities.map(identity => ({
        name: [
            (identity.email ?? identity.identityKey).padEnd(idW),
            identity.isDefault ? chalk.green('default') : '       ',
            chalk.gray(identity.versions.join(', ')),
        ].join('  '),
        value: identity.identityKey,
    }));
    try {
        const key = await select({ message: `Select the ${agent} login to label:`, choices, loop: false });
        return identities.find(identity => identity.identityKey === key) ?? null;
    }
    catch (err) {
        if (isPromptCancelled(err))
            return null;
        throw err;
    }
}
/** Every account (native + provider) registered/usable for this harness, oldest-first by name. */
function accountsForHarness(agent) {
    const meta = readMeta();
    const native = listNativeAccounts(meta).filter(account => account.agent === agent);
    const providers = Object.values(readAccountRegistry().accounts)
        .filter(account => providerAuthenticatesHarness(account.provider, account.auth, agent))
        .map(account => ({ ...account, kind: 'provider' }));
    return [...native, ...providers].sort((a, b) => a.name.localeCompare(b.name));
}
/** Named accounts that `set-default` / `switch` can pin for this harness. */
export async function listSwitchableAccounts(agent) {
    return accountsForHarness(agent);
}
/**
 * Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
 * Provider accounts must authenticate the harness; native accounts must belong to it.
 */
export function setDefaultAccount(agentRaw, name) {
    const agent = parseHarness(agentRaw);
    // Scope to the harness this default is FOR: a bare identity (`<email>`) matches
    // every harness that identity is signed into, so an un-scoped lookup resolved
    // the wrong row and then rejected it as "is a <other> login" just below.
    const account = findUnifiedAccount(name, readMeta(), undefined, agent);
    if (!account)
        throw new Error(`Unknown account '${name}'.`);
    if (account.kind === 'provider') {
        getAccountProvider(account.provider).envFor(agent, account.auth);
    }
    else {
        if (account.agent !== agent) {
            throw new Error(`Account '${account.name}' is a ${account.agent} login and cannot be the default for ${agent}.`);
        }
        assertNativeAccountNameable(account.agent);
    }
    // Reference by NAME, not id: defaults sync fleet-wide with `agents repo push/pull`
    // while account ids are minted per-device, so an id ref breaks on every other
    // machine ("Unknown account '<uuid>'"). Names are the portable handle — the
    // registry resolves both, and existing uuid entries still resolve.
    updateMeta(meta => ({ ...meta, accounts: { ...meta.accounts, defaults: { ...meta.accounts?.defaults, [agent]: account.name } } }));
    return { agent, account };
}
async function switchAccountRows(agent) {
    const accounts = await listSwitchableAccounts(agent);
    const candidates = await collectRunCandidates(agent);
    const defaultValue = readMeta().accounts?.defaults?.[agent];
    return accounts.map(account => {
        const candidate = account.kind === 'native'
            ? candidates.find(row => row.accountKey === account.identityKey) ?? null
            : null;
        return {
            accountName: account.name,
            kind: account.kind,
            detail: account.kind === 'provider' ? account.provider : (account.identityLabel ?? account.identityKey),
            current: account.id === defaultValue || account.name === defaultValue,
            candidate,
        };
    });
}
export async function runAccountsSwitch(harness, accountName, opts) {
    const agent = parseHarness(harness);
    const rows = await switchAccountRows(agent);
    if (accountName) {
        const { account } = setDefaultAccount(agent, accountName);
        if (opts.json) {
            console.log(JSON.stringify({ harness: agent, account: account.name, id: account.id }, null, 2));
            return;
        }
        console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
        return;
    }
    if (opts.json) {
        const choices = buildSwitchAccountChoices(rows);
        console.log(JSON.stringify({
            harness: agent,
            default: rows.find(row => row.current)?.accountName ?? null,
            accounts: rows.map((row, i) => ({
                name: row.accountName,
                kind: row.kind,
                detail: row.detail,
                current: row.current,
                ready: choices[i].ready,
                limits: row.candidate ? formatAccountLimits(row.candidate) : 'limits unavailable',
            })),
        }, null, 2));
        return;
    }
    if (!isInteractiveTerminal()) {
        throw new Error(`Selecting a ${agent} account requires an interactive terminal.\nUse: agents accounts switch ${agent} <account>\nOr:  agents accounts set-default ${agent} <account>`);
    }
    const selected = await pickSwitchAccount(agent, rows);
    if (!selected)
        return;
    const { account } = setDefaultAccount(agent, selected);
    console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
}
export function registerAccountsCommand(program) {
    const accounts = program.command('accounts').description('Browse native logins and manage provider account bundles')
        .option('--json', 'Machine-readable account metadata')
        .option('--fleet', 'Show harness-native signed-in identities across reachable devices')
        .action(async (o, command) => {
        await runAccountsAction(command, () => printAccounts(!!o.json, !!o.fleet));
    });
    accounts.command('list').description('List credential accounts').option('--json', 'Machine-readable account metadata').action(async (o, command) => {
        await runAccountsAction(command, () => printAccounts(!!(o.json || command.optsWithGlobals().json)));
    });
    registerMintCommand(accounts);
    accounts.command('add <name>')
        .description('Add a durable API key, setup token, or bearer token')
        .requiredOption('--provider <provider>', `Credential provider (${listAccountProviders().join(', ')})`)
        .requiredOption('--auth <type>', 'Credential type: api-key | setup-token | bearer-token')
        .option('--base-url <url>', 'Optional endpoint override stored with the account')
        .option('--from-secrets <bundle:key>', 'Import from an existing agents secrets entry')
        .action(async (name, o, command) => {
        await runAccountsAction(command, async () => {
            const auth = parseAuth(o.auth);
            const provider = getAccountProvider(o.provider);
            if (!provider.authKinds.includes(auth))
                throw new Error(`Provider '${provider.provider}' does not support ${auth}. Supported: ${provider.authKinds.join(', ')}.`);
            const secret = o.fromSecrets
                ? secretFromBundle(o.fromSecrets)
                : await promptAccountSecret(`Enter ${provider.provider} ${auth} for '${name}':`);
            if (secret === null) {
                process.exit(130);
                return;
            }
            const account = addAccount(name, provider.provider, auth, secret, undefined, { baseUrl: o.baseUrl });
            console.log(chalk.green(`Added ${account.provider} ${account.auth} account '${account.name}'.`));
            console.log(chalk.gray(`Secret bundle '${account.name}' is the account and uses policy never, so agent launches never request Touch ID.`));
        });
    });
    accounts.command('set-key <name>')
        .description('Rotate an account credential without changing its identity')
        .option('--from-secrets <bundle:key>', 'Import from an existing agents secrets entry')
        .action(async (name, o, command) => {
        await runAccountsAction(command, async () => {
            const account = findAccount(name);
            if (!account)
                throw new Error(`Unknown provider account '${name}'.`);
            const secret = o.fromSecrets
                ? secretFromBundle(o.fromSecrets)
                : await promptAccountSecret(`Enter new ${account.provider} ${account.auth} for '${name}':`);
            if (secret === null) {
                process.exit(130);
                return;
            }
            setAccountSecret(name, secret);
            console.log(chalk.green(`Updated credential for account '${name}'.`));
        });
    });
    accounts.command('view <name>').alias('inspect').description('Show safe account metadata, custody, and attachments').option('--json', 'Machine-readable output').action(async (name, o, command) => {
        await runAccountsAction(command, async () => {
            const meta = readMeta();
            const unified = findUnifiedAccount(name, meta);
            if (!unified)
                throw new Error(`Unknown account '${name}'.`);
            const account = unified.kind === 'provider'
                ? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
                : { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
            if (o.json || command.optsWithGlobals().json)
                return console.log(JSON.stringify(account, null, 2));
            console.log(chalk.bold(account.name));
            console.log(`  kind: ${account.kind}`);
            console.log(`  id: ${account.id}`);
            console.log(`  custody: ${account.custody}`);
            if (account.kind === 'provider') {
                console.log(`  provider: ${account.provider}`);
                console.log(`  auth: ${account.auth}`);
                console.log(`  credential: ${account.secretPresent ? 'present on this device' : 'missing on this device'}`);
            }
            else {
                console.log(`  identity: ${account.identityLabel ?? account.identityKey}`);
                console.log(`  scope: ${account.scope}`);
            }
            console.log(`  attached: ${account.attached.length ? account.attached.join(', ') : 'none'}`);
        });
    });
    accounts.command('name <source> <name>')
        .description('Name a signed-in native installation without copying its OAuth credentials')
        .action(async (source, name, _o, command) => {
        await runAccountsAction(command, async () => {
            const identity = await nativeIdentityFromSource(source);
            const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
            console.log(chalk.green(`Named ${source} as ${account.name}.`));
            if (account.scope === 'device')
                console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
        });
    });
    const labelCmd = accounts.command('label <source> [label]')
        .description('Label a native login by harness or <harness>@<version>; the label binds to the account identity, not the version')
        .option('--account <email-or-id>', 'Native identity to label when the harness has multiple logins')
        .action(async (source, label, o, command) => {
        await runAccountsAction(command, () => runAccountsLabel(source, label, o));
    });
    setHelpSections(labelCmd, {
        examples: `agents accounts label codex work
agents accounts label codex@0.146.0 personal
agents accounts label codex work --account you@example.com
agents run codex#work`,
        notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. Labels live on the central account rows in agents.yaml, which `agents repo push/pull` already syncs fleet-wide, keyed by (agent, identityKey). One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
    });
    accounts.command('attach <account> <target>')
        .description('Attach a named account to a native installation or custom harness')
        .action(async (name, target, _o, command) => {
        await runAccountsAction(command, async () => {
            const meta = readMeta();
            // Validate the target exists before mutating any binding — and resolve it
            // FIRST so the account lookup can be scoped to the harness being attached
            // to. `identityLabel` defaults to the login's email, so a bare identity
            // (`muqsitnawaz@gmail.com`) matches every harness that identity is signed
            // into; un-scoped this resolved whichever row the store ordered first and
            // then rejected it below as "is a <other> login".
            const t = classifyAttachTarget(target);
            const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
            const account = findUnifiedAccount(name, meta, undefined, targetAgent);
            if (!account)
                throw new Error(`Unknown account '${name}'.`);
            if (account.kind === 'native')
                assertNativeAccountNameable(account.agent);
            if (account.kind === 'native') {
                // A provider-backed profile injects provider env at spawn, which would run
                // under a different credential than the native identity claims — refuse it.
                if (t.kind === 'profile' && t.profile.provider) {
                    throw new Error(`Custom harness '${target}' is provider-backed (${t.profile.provider}); it cannot host the native login account '${account.name}'. Attach a matching provider account instead.`);
                }
                if (targetAgent !== account.agent)
                    throw new Error(`Account '${account.name}' is a ${account.agent} login; '${target}' runs ${targetAgent}.`);
                if (account.scope === 'device') {
                    if (t.kind !== 'device-agent')
                        throw new Error(`${account.agent} authentication is device-scoped. Attach it with 'agents accounts attach ${account.name} ${account.agent}'.`);
                }
                else {
                    if (t.kind !== 'installation')
                        throw new Error(`${account.agent} authentication is per-version. Attach '${account.name}' to a specific ${account.agent}@<version>.`);
                    const versionHome = getVersionHomePath(t.agent, t.version);
                    // The literal email keys the account's `auth`-bundle setup-token. It lives
                    // in `identityLabel` — `identityKey` is a synthetic composite
                    // (`claude:account=<uuid>:org=<uuid>`, agents.ts nativeIdentityKey), never
                    // the address, so it must NOT be used to derive the token key.
                    const accountEmail = account.identityLabel;
                    // Headless-worker bootstrap: a keychain-less Linux worker home never had
                    // an interactive login, so its `.claude.json` carries no identity and
                    // `nativeIdentityFromSource` would reject the attach — yet the account's
                    // non-rotating setup-token is already fleet-synced in the `auth` bundle.
                    // Seed the identity (email only, no rotating credential) so the token
                    // resolves; `writeClaudeInteractiveOauthToken` then writes `.oauth_token`.
                    if (process.platform === 'linux' &&
                        account.agent === 'claude' &&
                        accountEmail &&
                        !readClaudeAccountEmail(versionHome) &&
                        resolveClaudeSetupTokenForEmail(accountEmail)) {
                        seedClaudeWorkerHomeIdentity(versionHome, accountEmail);
                    }
                    else {
                        const identity = await nativeIdentityFromSource(target);
                        if (identity.identityKey !== account.identityKey)
                            throw new Error(`'${target}' is signed in to a different identity than account '${account.name}'.`);
                    }
                }
            }
            else {
                // Provider account: it must be able to authenticate the target's harness.
                getAccountProvider(account.provider).envFor(targetAgent, account.auth);
            }
            bindAccount(name, target, targetAgent);
            writeClaudeInteractiveOauthToken(t, targetAgent, account.kind === 'native' && account.agent === 'claude' ? account.identityLabel : undefined);
            console.log(chalk.green(`Attached ${account.name} to ${target}.`));
        });
    });
    accounts.command('detach <account> <target>')
        .description('Remove one account attachment')
        .action(async (name, target, _o, command) => {
        await runAccountsAction(command, () => {
            // Classify the target first so the unbind is scoped to the right harness
            // for a colliding identity selector (same as the attach path).
            let targetAgent;
            try {
                const t = classifyAttachTarget(target);
                targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
            }
            catch { /* an unresolvable target still unbinds by whatever row matches */ }
            unbindAccount(name, target, targetAgent);
            // With the binding gone, no setup-token resolves for this version home, so this
            // clears any .oauth_token the attach left behind (else interactive runs would keep
            // authenticating as the just-detached account).
            try {
                const t = classifyAttachTarget(target);
                writeClaudeInteractiveOauthToken(t, t.kind === 'profile' ? t.profile.host.agent : t.agent);
            }
            catch { /* an unresolvable target has no version home to clean */ }
            console.log(chalk.green(`Detached ${name} from ${target}.`));
        });
    });
    accounts.command('rename <old> <new>').description('Rename an account without changing its stable id').action(async (oldName, newName, _o, command) => {
        await runAccountsAction(command, () => { renameAccount(oldName, newName); });
    });
    accounts.command('remove <name>').description('Remove an account and its device-local credential').action(async (name, _o, command) => {
        await runAccountsAction(command, () => { removeAccount(name); });
    });
    accounts.command('set-default <agent> <name>')
        .description('Use this account for a harness when --account is omitted')
        .action(async (agentRaw, name, _o, command) => {
        await runAccountsAction(command, () => {
            const { agent, account } = setDefaultAccount(agentRaw, name);
            console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
        });
    });
    const switchCmd = accounts.command('switch <harness> [account]')
        .description('Pick the default account for a harness')
        .option('--json', 'Machine-readable account list or the resulting default')
        .action(async (harness, account, o, command) => {
        await runAccountsAction(command, () => runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) }));
    });
    accounts.command('clear-default <agent>')
        .description('Return a harness to native login or balanced account selection')
        .action(async (agentRaw, _o, command) => {
        await runAccountsAction(command, () => {
            parseHarness(agentRaw);
            updateMeta(meta => {
                const defaults = { ...meta.accounts?.defaults };
                delete defaults[agentRaw];
                return { ...meta, accounts: { ...meta.accounts, defaults } };
            });
            console.log(chalk.green(`Cleared the default account for ${agentRaw}.`));
        });
    });
    accounts.command('logout <target>')
        .description('Sign out a harness-native OAuth login. API-key / setup-token / bearer accounts use `accounts remove` instead.')
        .action(async (target, _o, command) => {
        await runAccountsAction(command, async () => {
            const provider = findAccount(target);
            if (provider) {
                throw new Error(`Account '${provider.name}' uses ${provider.auth}, not OAuth. Remove it with: agents accounts remove ${provider.name}`);
            }
            const agentRaw = target.split('@')[0];
            if (!ALL_AGENT_IDS.includes(agentRaw)) {
                throw new Error(`Unknown target '${target}'. Pass a native harness (claude, codex, …) or a provider account name. Provider API-key accounts use \`agents accounts remove\`.`);
            }
            const agent = agentRaw;
            const { spawnSync } = await import('child_process');
            const { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } = await import('../lib/installations/versions.js');
            const installed = listInstalledVersions(agent);
            const version = getGlobalDefault(agent) ?? installed[installed.length - 1];
            if (!version)
                throw new Error(`No installed version of ${agent}. Install one with: agents add ${agent}`);
            const bin = getBinaryPath(agent, version);
            const home = getVersionHomePath(agent, version);
            const result = spawnSync(bin, ['logout'], {
                env: { ...process.env, HOME: home },
                stdio: 'inherit',
            });
            if (result.error)
                throw result.error;
            if ((result.status ?? 1) !== 0) {
                throw new Error(`${agent} logout exited ${result.status ?? 'null'}. If this harness has no logout verb, sign out from its own UI.`);
            }
            console.log(chalk.green(`Signed out native ${agent} login (${version}).`));
        });
    });
    accounts.command('sync <name> [device]')
        .description('Copy one provider account bundle to a worker device')
        .option('--device <device>', 'Deprecated destination form; use the positional device')
        .option('--force', 'Replace matching keys on the destination')
        .action(async (name, deviceArg, o, command) => {
        await runAccountsAction(command, async () => {
            const device = deviceArg ?? o.device;
            if (!device)
                throw new Error('Missing destination device. Usage: agents accounts sync <account> <device>.');
            const account = findAccount(name);
            if (!account)
                throw new Error(`Unknown provider account '${name}'.`);
            const sshTarget = await resolveHostSshTarget(device);
            assertCredentialTransportHostPinned(sshTarget);
            const remoteBackend = resolveRemoteOsSync(device) === 'win32' ? 'keychain' : 'file';
            const literalValues = {
                ACCOUNT_ID: account.id,
                PROVIDER: account.provider,
                AUTH_TYPE: account.auth,
                ...(account.baseUrl ? { BASE_URL: account.baseUrl } : {}),
            };
            const result = pushBundleToHost(account.name, device, {
                remoteBackend,
                force: o.force,
                operation: 'accounts sync',
                policyNever: true,
                agentOnly: false,
                literalValues,
            });
            if (!result.ok)
                throw new Error(`${result.message}\nRetry: agents accounts sync ${account.name} ${device}${o.force ? ' --force' : ''}`);
            console.log(chalk.green(`${account.name} synced to ${device} (${result.keyCount} keys, ${remoteBackend} backend, policy never).`));
        });
    });
    setHelpSections(switchCmd, {
        examples: `agents accounts switch claude
agents accounts switch claude work
agents accounts switch claude --json
agents run claude`,
        notes: 'Switch writes the existing per-harness default (same binding as set-default). Rotation already honors it. Pass an account name to skip the picker. Native name/attach is only for harnesses agents-cli can isolate (claude, codex, grok); provider add is unrestricted.',
    });
    setHelpSections(accounts, {
        examples: `agents accounts mint claude
agents accounts mint claude --account work --email ada@example.com --fleet
agents accounts add work --provider anthropic --auth setup-token
agents accounts add openrouter-work --provider openrouter --auth api-key --from-secrets openrouter.ai:OPENROUTER_API_KEY
agents accounts set-key work
agents accounts name claude@2.1.220 work
agents accounts label codex@0.146.0 personal
agents accounts attach work claude@2.1.225
agents accounts view work --json
agents accounts switch claude
agents accounts switch claude work
agents accounts set-default claude work
agents accounts sync openrouter-work yosemite-s0
agents run claude --account work
agents accounts logout claude`,
        notes: 'Native account records contain metadata only; harness-owned OAuth credentials are never copied. Provider accounts are explicit portable bundles with policy never. `accounts mint claude` drives `claude setup-token` and seeds both the named account and the reserved file-based auth bundle (same command as `agents auth mint`). `accounts switch` is the fast picker over the same default `set-default` writes. Harness-native OAuth sign-out is `agents accounts logout <harness>` (API-key accounts use `accounts remove`). Synced vault unlock is `agents secrets vault unlock`.',
    });
}
