import { c as _c } from "react/compiler-runtime"; import { execa } from 'execa'; import { readFile } from 'fs/promises'; import { join } from 'path'; import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import type { CommandResultDisplay } from '../../commands.js'; import { Select } from '../../components/CustomSelect/select.js'; import { Dialog } from '../../components/design-system/Dialog.js'; import { Spinner } from '../../components/Spinner.js'; import instances from '../../ink/instances.js'; import { Box, Text } from '../../ink.js'; import { enablePluginOp } from '../../services/plugins/pluginOperations.js'; import { logForDebugging } from '../../utils/debug.js'; import { isENOENT, toError } from '../../utils/errors.js'; import { execFileNoThrow } from '../../utils/execFileNoThrow.js'; import { pathExists } from '../../utils/file.js'; import { logError } from '../../utils/log.js'; import { getPlatform } from '../../utils/platform.js'; import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'; import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js'; import { addMarketplaceSource, clearMarketplacesCache, loadKnownMarketplacesConfig, refreshMarketplace } from '../../utils/plugins/marketplaceManager.js'; import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js'; import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'; import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js'; // Marketplace and plugin identifiers - varies by user type const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace'; const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace'; const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official'; function getMarketplaceName(): string { return "external" === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME; } function getMarketplaceRepo(): string { return "external" === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO; } function getPluginId(): string { return `thinkback@${getMarketplaceName()}`; } const SKILL_NAME = 'thinkback'; /** * Get the thinkback skill directory from the installed plugin's cache path */ async function getThinkbackSkillDir(): Promise { const { enabled } = await loadAllPlugins(); const thinkbackPlugin = enabled.find(p => p.name === 'thinkback' || p.source && p.source.includes(getPluginId())); if (!thinkbackPlugin) { return null; } const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME); if (await pathExists(skillDir)) { return skillDir; } return null; } export async function playAnimation(skillDir: string): Promise<{ success: boolean; message: string; }> { const dataPath = join(skillDir, 'year_in_review.js'); const playerPath = join(skillDir, 'player.js'); // Both files are prerequisites for the node subprocess. Read them here // (not at call sites) so all callers get consistent error messaging. The // subprocess runs with reject: false, so a missing file would otherwise // silently return success. Using readFile (not access) per CLAUDE.md. // // Non-ENOENT errors (EACCES etc) are logged and returned as failures rather // than thrown — the old pathExists-based code never threw, and one caller // (handleSelect) uses `void playAnimation().then(...)` without a .catch(). try { await readFile(dataPath); } catch (e: unknown) { if (isENOENT(e)) { return { success: false, message: 'No animation found. Run /think-back first to generate one.' }; } logError(e); return { success: false, message: `Could not access animation data: ${toError(e).message}` }; } try { await readFile(playerPath); } catch (e: unknown) { if (isENOENT(e)) { return { success: false, message: 'Player script not found. The player.js file is missing from the thinkback skill.' }; } logError(e); return { success: false, message: `Could not access player script: ${toError(e).message}` }; } // Get ink instance for terminal takeover const inkInstance = instances.get(process.stdout); if (!inkInstance) { return { success: false, message: 'Failed to access terminal instance' }; } inkInstance.enterAlternateScreen(); try { await execa('node', [playerPath], { stdio: 'inherit', cwd: skillDir, reject: false }); } catch { // Animation may have been interrupted (e.g., Ctrl+C) } finally { inkInstance.exitAlternateScreen(); } // Open the HTML file in browser for video download const htmlPath = join(skillDir, 'year_in_review.html'); if (await pathExists(htmlPath)) { const platform = getPlatform(); const openCmd = platform === 'macos' ? 'open' : platform === 'windows' ? 'start' : 'xdg-open'; void execFileNoThrow(openCmd, [htmlPath]); } return { success: true, message: 'Year in review animation complete!' }; } type InstallState = { phase: 'checking'; } | { phase: 'installing-marketplace'; } | { phase: 'installing-plugin'; } | { phase: 'enabling-plugin'; } | { phase: 'ready'; } | { phase: 'error'; message: string; }; function ThinkbackInstaller({ onReady, onError }: { onReady: () => void; onError: (message: string) => void; }): React.ReactNode { const [state, setState] = useState({ phase: 'checking' }); const [progressMessage, setProgressMessage] = useState(''); useEffect(() => { async function checkAndInstall(): Promise { try { // Check if marketplace is installed const knownMarketplaces = await loadKnownMarketplacesConfig(); const marketplaceName = getMarketplaceName(); const marketplaceRepo = getMarketplaceRepo(); const pluginId = getPluginId(); const marketplaceInstalled = marketplaceName in knownMarketplaces; // Check if plugin is already installed first const pluginAlreadyInstalled = isPluginInstalled(pluginId); if (!marketplaceInstalled) { // Install the marketplace setState({ phase: 'installing-marketplace' }); logForDebugging(`Installing marketplace ${marketplaceRepo}`); await addMarketplaceSource({ source: 'github', repo: marketplaceRepo }, message => { setProgressMessage(message); }); clearAllCaches(); logForDebugging(`Marketplace ${marketplaceName} installed`); } else if (!pluginAlreadyInstalled) { // Marketplace installed but plugin not installed - refresh to get latest plugins // Only refresh when needed to avoid potentially destructive git operations setState({ phase: 'installing-marketplace' }); setProgressMessage('Updating marketplace…'); logForDebugging(`Refreshing marketplace ${marketplaceName}`); await refreshMarketplace(marketplaceName, message_0 => { setProgressMessage(message_0); }); clearMarketplacesCache(); clearAllCaches(); logForDebugging(`Marketplace ${marketplaceName} refreshed`); } if (!pluginAlreadyInstalled) { // Install the plugin setState({ phase: 'installing-plugin' }); logForDebugging(`Installing plugin ${pluginId}`); const result = await installSelectedPlugins([pluginId]); if (result.failed.length > 0) { const errorMsg = result.failed.map(f => `${f.name}: ${f.error}`).join(', '); throw new Error(`Failed to install plugin: ${errorMsg}`); } clearAllCaches(); logForDebugging(`Plugin ${pluginId} installed`); } else { // Plugin is installed, check if it's enabled const { disabled } = await loadAllPlugins(); const isDisabled = disabled.some(p => p.name === 'thinkback' || p.source?.includes(pluginId)); if (isDisabled) { // Enable the plugin setState({ phase: 'enabling-plugin' }); logForDebugging(`Enabling plugin ${pluginId}`); const enableResult = await enablePluginOp(pluginId); if (!enableResult.success) { throw new Error(`Failed to enable plugin: ${enableResult.message}`); } clearAllCaches(); logForDebugging(`Plugin ${pluginId} enabled`); } } setState({ phase: 'ready' }); onReady(); } catch (error) { const err = toError(error); logError(err); setState({ phase: 'error', message: err.message }); onError(err.message); } } void checkAndInstall(); }, [onReady, onError]); if (state.phase === 'error') { return Error: {state.message} ; } if (state.phase === 'ready') { return null; } const statusMessage = state.phase === 'checking' ? 'Checking thinkback installation…' : state.phase === 'installing-marketplace' ? 'Installing marketplace…' : state.phase === 'enabling-plugin' ? 'Enabling thinkback plugin…' : 'Installing thinkback plugin…'; return {progressMessage || statusMessage} ; } type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate'; type GenerativeAction = Exclude; function ThinkbackMenu(t0) { const $ = _c(19); const { onDone, onAction, skillDir, hasGenerated } = t0; const [hasSelected, setHasSelected] = useState(false); let t1; if ($[0] !== hasGenerated) { t1 = hasGenerated ? [{ label: "Play animation", value: "play" as const, description: "Watch your year in review" }, { label: "Edit content", value: "edit" as const, description: "Modify the animation" }, { label: "Fix errors", value: "fix" as const, description: "Fix validation or rendering issues" }, { label: "Regenerate", value: "regenerate" as const, description: "Create a new animation from scratch" }] : [{ label: "Let's go!", value: "regenerate" as const, description: "Generate your personalized animation" }]; $[0] = hasGenerated; $[1] = t1; } else { t1 = $[1]; } const options = t1; let t2; if ($[2] !== onAction || $[3] !== onDone || $[4] !== skillDir) { t2 = function handleSelect(value) { setHasSelected(true); if (value === "play") { playAnimation(skillDir).then(() => { onDone(undefined, { display: "skip" }); }); } else { onAction(value); } }; $[2] = onAction; $[3] = onDone; $[4] = skillDir; $[5] = t2; } else { t2 = $[5]; } const handleSelect = t2; let t3; if ($[6] !== onDone) { t3 = function handleCancel() { onDone(undefined, { display: "skip" }); }; $[6] = onDone; $[7] = t3; } else { t3 = $[7]; } const handleCancel = t3; if (hasSelected) { return null; } let t4; if ($[8] !== hasGenerated) { t4 = !hasGenerated && Relive your year of coding with Claude.{"We'll create a personalized ASCII animation celebrating your journey."}; $[8] = hasGenerated; $[9] = t4; } else { t4 = $[9]; } let t5; if ($[10] !== handleSelect || $[11] !== options) { t5 =