mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
* plugins: commands[].replaces lets a worker command take over a built-in action A worker command with replaces: <action> (the declarative action list, engines >=1.4.248, needs main) runs in place of that built-in from its shortcut, plugin aliases and the sidebar menus; a failed or silent (2 s) worker falls back to Orca's own. The install screen names each replacement, the plugin page has a per-action switch (settings.pluginReplacementsOff), and when two plugins replace one action the one installed first wins and the other's log says so. Update review counts replaces as new access. * fix(plugins): narrow replaced action ids before looking up their titles * plugins: harden command replaces after review - Consent includes the replaced actions, so adding replaces to a dev folder needs a new approval; CLI approval and update review name each takeover. - Shortcuts main intercepts (Tasks, Workspace Board, history), search with a selection and floating maximize now go through the replacement too. - A worker that only starts after the 2 s fallback no longer runs the command as well: main refuses to start it past the deadline. - A workspace-only command does not replace an action without a workspace. - An update keeps the plugin's first install time, so it keeps its takeover. - Drop the commands.run promise until Orca offers it. * plugins: commands.run always runs the built-in, never a replacement A5b's commands.run dispatches with the 'plugin-host' source; the app dispatcher now builds handlers that run Orca's own action for it, so a plugin that replaces an action and calls commands.run cannot loop. Docs and schema describe wrapping the built-in with commands.run; regenerated.
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
|
|
import type { PluginLockfile } from '../../shared/plugins/installation/plugin-install-lockfile'
|
|
|
|
const LIVE = new Set(['running', 'idle', 'restarting'])
|
|
// Conflicts already logged, so a list refresh does not repeat them.
|
|
const logged = new Set<string>()
|
|
|
|
/**
|
|
* One plugin per built-in action: the one installed first replaces it, later ones are marked
|
|
* `replacedBy` and the conflict is logged to each loser's plugin log. Development folders, which
|
|
* have no install time, come after installed plugins.
|
|
*/
|
|
export function markCommandReplacementOwners(
|
|
entries: PluginHostListEntry[],
|
|
lock: Pick<PluginLockfile, 'plugins'>,
|
|
log: (pluginKey: string, line: string) => void
|
|
): PluginHostListEntry[] {
|
|
const installedAt = (key: string) => lock.plugins[key]?.installedAt ?? Number.POSITIVE_INFINITY
|
|
const owners = new Map<string, string>()
|
|
const contenders = entries
|
|
.filter((entry) => LIVE.has(entry.status))
|
|
.toSorted(
|
|
(a, b) =>
|
|
installedAt(a.pluginKey) - installedAt(b.pluginKey) ||
|
|
a.pluginKey.localeCompare(b.pluginKey)
|
|
)
|
|
for (const entry of contenders) {
|
|
for (const command of entry.commands) {
|
|
if (command.replaces !== undefined && !owners.has(command.replaces)) {
|
|
owners.set(command.replaces, entry.pluginKey)
|
|
}
|
|
}
|
|
}
|
|
return entries.map((entry) => {
|
|
if (!LIVE.has(entry.status) || !entry.commands.some((command) => command.replaces)) {
|
|
return entry
|
|
}
|
|
const commands = entry.commands.map((command) => {
|
|
const owner = command.replaces === undefined ? undefined : owners.get(command.replaces)
|
|
if (owner === undefined || owner === entry.pluginKey) {
|
|
return command
|
|
}
|
|
const conflict = `${entry.pluginKey}/${command.id}\0${owner}`
|
|
if (!logged.has(conflict)) {
|
|
logged.add(conflict)
|
|
log(
|
|
entry.pluginKey,
|
|
`command ${command.id} does not replace ${command.replaces}: ${owner} was installed first and replaces it`
|
|
)
|
|
}
|
|
return { ...command, replacedBy: owner }
|
|
})
|
|
return { ...entry, commands }
|
|
})
|
|
}
|