chore: initialize recovered claude workspace

This commit is contained in:
2026-04-02 15:29:01 +08:00
commit a10efa3b4b
1940 changed files with 506426 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
/**
* Background plugin and marketplace installation manager
*
* This module handles automatic installation of plugins and marketplaces
* from trusted sources (repository and user settings) without blocking startup.
*/
import type { AppState } from '../../state/AppState.js'
import { logForDebugging } from '../../utils/debug.js'
import { logForDiagnosticsNoPII } from '../../utils/diagLogs.js'
import { logError } from '../../utils/log.js'
import {
clearMarketplacesCache,
getDeclaredMarketplaces,
loadKnownMarketplacesConfig,
} from '../../utils/plugins/marketplaceManager.js'
import { clearPluginCache } from '../../utils/plugins/pluginLoader.js'
import {
diffMarketplaces,
reconcileMarketplaces,
} from '../../utils/plugins/reconciler.js'
import { refreshActivePlugins } from '../../utils/plugins/refresh.js'
import { logEvent } from '../analytics/index.js'
type SetAppState = (f: (prevState: AppState) => AppState) => void
/**
* Update marketplace installation status in app state
*/
function updateMarketplaceStatus(
setAppState: SetAppState,
name: string,
status: 'pending' | 'installing' | 'installed' | 'failed',
error?: string,
): void {
setAppState(prevState => ({
...prevState,
plugins: {
...prevState.plugins,
installationStatus: {
...prevState.plugins.installationStatus,
marketplaces: prevState.plugins.installationStatus.marketplaces.map(
m => (m.name === name ? { ...m, status, error } : m),
),
},
},
}))
}
/**
* Perform background plugin startup checks and installations.
*
* This is a thin wrapper around reconcileMarketplaces() that maps onProgress
* events to AppState updates for the REPL UI. After marketplaces are
* reconciled:
* - New installs → auto-refresh plugins (fixes "plugin-not-found" errors
* from the initial cache-only load on fresh homespace/cleared cache)
* - Updates only → set needsRefresh, show notification for /reload-plugins
*/
export async function performBackgroundPluginInstallations(
setAppState: SetAppState,
): Promise<void> {
logForDebugging('performBackgroundPluginInstallations called')
try {
// Compute diff upfront for initial UI status (pending spinners)
const declared = getDeclaredMarketplaces()
const materialized = await loadKnownMarketplacesConfig().catch(() => ({}))
const diff = diffMarketplaces(declared, materialized)
const pendingNames = [
...diff.missing,
...diff.sourceChanged.map(c => c.name),
]
// Initialize AppState with pending status. No per-plugin pending status —
// plugin load is fast (cache hit or local copy); marketplace clone is the
// slow part worth showing progress for.
setAppState(prev => ({
...prev,
plugins: {
...prev.plugins,
installationStatus: {
marketplaces: pendingNames.map(name => ({
name,
status: 'pending' as const,
})),
plugins: [],
},
},
}))
if (pendingNames.length === 0) {
return
}
logForDebugging(
`Installing ${pendingNames.length} marketplace(s) in background`,
)
const result = await reconcileMarketplaces({
onProgress: event => {
switch (event.type) {
case 'installing':
updateMarketplaceStatus(setAppState, event.name, 'installing')
break
case 'installed':
updateMarketplaceStatus(setAppState, event.name, 'installed')
break
case 'failed':
updateMarketplaceStatus(
setAppState,
event.name,
'failed',
event.error,
)
break
}
},
})
const metrics = {
installed_count: result.installed.length,
updated_count: result.updated.length,
failed_count: result.failed.length,
up_to_date_count: result.upToDate.length,
}
logEvent('tengu_marketplace_background_install', metrics)
logForDiagnosticsNoPII(
'info',
'tengu_marketplace_background_install',
metrics,
)
if (result.installed.length > 0) {
// New marketplaces were installed — auto-refresh plugins. This fixes
// "Plugin not found in marketplace" errors from the initial cache-only
// load (e.g., fresh homespace where marketplace cache was empty).
// refreshActivePlugins clears all caches, reloads plugins, and bumps
// pluginReconnectKey so MCP connections are re-established.
clearMarketplacesCache()
logForDebugging(
`Auto-refreshing plugins after ${result.installed.length} new marketplace(s) installed`,
)
try {
await refreshActivePlugins(setAppState)
} catch (refreshError) {
// If auto-refresh fails, fall back to needsRefresh notification so
// the user can manually run /reload-plugins to recover.
logError(refreshError)
logForDebugging(
`Auto-refresh failed, falling back to needsRefresh: ${refreshError}`,
{ level: 'warn' },
)
clearPluginCache(
'performBackgroundPluginInstallations: auto-refresh failed',
)
setAppState(prev => {
if (prev.plugins.needsRefresh) return prev
return {
...prev,
plugins: { ...prev.plugins, needsRefresh: true },
}
})
}
} else if (result.updated.length > 0) {
// Existing marketplaces updated — notify user to run /reload-plugins.
// Updates are less urgent and the user should choose when to apply them.
clearMarketplacesCache()
clearPluginCache(
'performBackgroundPluginInstallations: marketplaces reconciled',
)
setAppState(prev => {
if (prev.plugins.needsRefresh) return prev
return {
...prev,
plugins: { ...prev.plugins, needsRefresh: true },
}
})
}
} catch (error) {
logError(error)
}
}

View File

@@ -0,0 +1,344 @@
/**
* CLI command wrappers for plugin operations
*
* This module provides thin wrappers around the core plugin operations
* that handle CLI-specific concerns like console output and process exit.
*
* For the core operations (without CLI side effects), see pluginOperations.ts
*/
import figures from 'figures'
import { errorMessage } from '../../utils/errors.js'
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
import { logError } from '../../utils/log.js'
import { getManagedPluginNames } from '../../utils/plugins/managedPlugins.js'
import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js'
import type { PluginScope } from '../../utils/plugins/schemas.js'
import { writeToStdout } from '../../utils/process.js'
import {
buildPluginTelemetryFields,
classifyPluginCommandError,
} from '../../utils/telemetry/pluginTelemetry.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
logEvent,
} from '../analytics/index.js'
import {
disableAllPluginsOp,
disablePluginOp,
enablePluginOp,
type InstallableScope,
installPluginOp,
uninstallPluginOp,
updatePluginOp,
VALID_INSTALLABLE_SCOPES,
VALID_UPDATE_SCOPES,
} from './pluginOperations.js'
export { VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES }
type PluginCliCommand =
| 'install'
| 'uninstall'
| 'enable'
| 'disable'
| 'disable-all'
| 'update'
/**
* Generic error handler for plugin CLI commands. Emits
* tengu_plugin_command_failed before exit so dashboards can compute a
* success rate against the corresponding success events.
*/
function handlePluginCommandError(
error: unknown,
command: PluginCliCommand,
plugin?: string,
): never {
logError(error)
const operation = plugin
? `${command} plugin "${plugin}"`
: command === 'disable-all'
? 'disable all plugins'
: `${command} plugins`
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
`${figures.cross} Failed to ${operation}: ${errorMessage(error)}`,
)
const telemetryFields = plugin
? (() => {
const { name, marketplace } = parsePluginIdentifier(plugin)
return {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
...buildPluginTelemetryFields(
name,
marketplace,
getManagedPluginNames(),
),
}
})()
: {}
logEvent('tengu_plugin_command_failed', {
command:
command as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
error_category: classifyPluginCommandError(
error,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...telemetryFields,
})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)
}
/**
* CLI command: Install a plugin non-interactively
* @param plugin Plugin identifier (name or plugin@marketplace)
* @param scope Installation scope: user, project, or local (defaults to 'user')
*/
export async function installPlugin(
plugin: string,
scope: InstallableScope = 'user',
): Promise<void> {
try {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Installing plugin "${plugin}"...`)
const result = await installPluginOp(plugin, scope)
if (!result.success) {
throw new Error(result.message)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.tick} ${result.message}`)
// _PROTO_* routes to PII-tagged plugin_name/marketplace_name BQ columns.
// Unredacted plugin_id was previously logged to general-access
// additional_metadata for all users — dropped in favor of the privileged
// column route.
const { name, marketplace } = parsePluginIdentifier(
result.pluginId || plugin,
)
logEvent('tengu_plugin_installed_cli', {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
scope: (result.scope ||
scope) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
install_source:
'cli-explicit' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()),
})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
} catch (error) {
handlePluginCommandError(error, 'install', plugin)
}
}
/**
* CLI command: Uninstall a plugin non-interactively
* @param plugin Plugin name or plugin@marketplace identifier
* @param scope Uninstall from scope: user, project, or local (defaults to 'user')
*/
export async function uninstallPlugin(
plugin: string,
scope: InstallableScope = 'user',
keepData = false,
): Promise<void> {
try {
const result = await uninstallPluginOp(plugin, scope, !keepData)
if (!result.success) {
throw new Error(result.message)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.tick} ${result.message}`)
const { name, marketplace } = parsePluginIdentifier(
result.pluginId || plugin,
)
logEvent('tengu_plugin_uninstalled_cli', {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
scope: (result.scope ||
scope) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()),
})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
} catch (error) {
handlePluginCommandError(error, 'uninstall', plugin)
}
}
/**
* CLI command: Enable a plugin non-interactively
* @param plugin Plugin name or plugin@marketplace identifier
* @param scope Optional scope. If not provided, finds the most specific scope for the current project.
*/
export async function enablePlugin(
plugin: string,
scope?: InstallableScope,
): Promise<void> {
try {
const result = await enablePluginOp(plugin, scope)
if (!result.success) {
throw new Error(result.message)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.tick} ${result.message}`)
const { name, marketplace } = parsePluginIdentifier(
result.pluginId || plugin,
)
logEvent('tengu_plugin_enabled_cli', {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
scope:
result.scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()),
})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
} catch (error) {
handlePluginCommandError(error, 'enable', plugin)
}
}
/**
* CLI command: Disable a plugin non-interactively
* @param plugin Plugin name or plugin@marketplace identifier
* @param scope Optional scope. If not provided, finds the most specific scope for the current project.
*/
export async function disablePlugin(
plugin: string,
scope?: InstallableScope,
): Promise<void> {
try {
const result = await disablePluginOp(plugin, scope)
if (!result.success) {
throw new Error(result.message)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.tick} ${result.message}`)
const { name, marketplace } = parsePluginIdentifier(
result.pluginId || plugin,
)
logEvent('tengu_plugin_disabled_cli', {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
scope:
result.scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()),
})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
} catch (error) {
handlePluginCommandError(error, 'disable', plugin)
}
}
/**
* CLI command: Disable all enabled plugins non-interactively
*/
export async function disableAllPlugins(): Promise<void> {
try {
const result = await disableAllPluginsOp()
if (!result.success) {
throw new Error(result.message)
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.tick} ${result.message}`)
logEvent('tengu_plugin_disabled_all_cli', {})
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
} catch (error) {
handlePluginCommandError(error, 'disable-all')
}
}
/**
* CLI command: Update a plugin non-interactively
* @param plugin Plugin name or plugin@marketplace identifier
* @param scope Scope to update
*/
export async function updatePluginCli(
plugin: string,
scope: PluginScope,
): Promise<void> {
try {
writeToStdout(
`Checking for updates for plugin "${plugin}" at ${scope} scope…\n`,
)
const result = await updatePluginOp(plugin, scope)
if (!result.success) {
throw new Error(result.message)
}
writeToStdout(`${figures.tick} ${result.message}\n`)
if (!result.alreadyUpToDate) {
const { name, marketplace } = parsePluginIdentifier(
result.pluginId || plugin,
)
logEvent('tengu_plugin_updated_cli', {
_PROTO_plugin_name:
name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
...(marketplace && {
_PROTO_marketplace_name:
marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
}),
old_version: (result.oldVersion ||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
new_version: (result.newVersion ||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
...buildPluginTelemetryFields(
name,
marketplace,
getManagedPluginNames(),
),
})
}
await gracefulShutdown(0)
} catch (error) {
handlePluginCommandError(error, 'update', plugin)
}
}

File diff suppressed because it is too large Load Diff