mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(plugin): a package's prompts and themes reach Studio
Both were parsed by pluginpkg and went nowhere, while the docs said they worked. A package's prompts are now discovered with its commands and invoked the same way, /<plugin>:<name>, which is what the manifest documented. Its themes are Studio theme packs: an entry names a pack's theme.json, and the theme library lists it as plugin:<plugin>:<directory>, read in place from the plugin root with its own images, never copied into the user's library, and gone when the plugin is disabled. The retired desktop's .reasonix-theme archives are not a format Studio reads, and the docs now say so. The plugin docs are brought in line with the code: the Studio installer takes a repository address or a folder (a .zip installs through the CLI), the routes Studio drives replace a list of Wails methods that no longer exist, and the protocol documents the panel and view surface kinds it already carries.
This commit is contained in:
1 parent
de375d26a5
commit
2c7bde8710
13 files changed
+255
-54
No files matched your search
+4
-2
@@ -54,8 +54,10 @@ install runtimes you trust completely.
|
||||
rendered natively in the CLI transcript, the Desktop app, and ACP clients
|
||||
(with text fallbacks), plus `/<plugin>:<action>` actions in the slash
|
||||
menu, the Desktop command palette, and ACP's discoverable commands.
|
||||
- **Prompts and themes** — `/<plugin>:<name>` prompt templates and
|
||||
read-only plugin themes (`plugin:<plugin>:<theme>`) in Desktop Settings.
|
||||
- **Prompts and themes** — `/<plugin>:<name>` prompt templates, discovered
|
||||
with the package's commands, and theme packs (`theme.json` directories)
|
||||
offered read-only in Studio's appearance settings as
|
||||
`plugin:<plugin>:<directory>`.
|
||||
|
||||
## Runtime reload
|
||||
|
||||
|
||||
@@ -110,8 +110,9 @@ Streams follow `extension/provider/stream/open` → `stream/chunk` →
|
||||
|
||||
## Structured UI
|
||||
|
||||
Extensions with the `ui` capability publish `status`, `card`, `form`, and
|
||||
`notification` payloads (`host/ui/publish`) and ask questions
|
||||
Extensions with the `ui` capability publish `status`, `card`, `form`,
|
||||
`notification`, `panel`, and `view` payloads (`host/ui/publish`) and ask
|
||||
questions
|
||||
(`host/ui/request`: confirm, input, select, multiselect). Surfaces are
|
||||
**structured only**: no HTML, CSS, JavaScript, remote scripts, arbitrary
|
||||
frontend components, or uncontrolled URLs; Markdown renders through each
|
||||
@@ -120,6 +121,11 @@ ID, surface ID, session ID, and runtime generation; stale-generation
|
||||
updates are dropped so late results after a tab switch or reload can never
|
||||
overwrite current state.
|
||||
|
||||
A `panel` holds a place in the frontend's side rail rather than scrolling
|
||||
past in the transcript: re-publishing the same surface id updates what is
|
||||
already on screen. A `view` is composed rather than filled in: its payload is a
|
||||
tree of host primitives, for a shape the fixed kinds do not have.
|
||||
|
||||
Actions declared at initialize are namespaced `/<plugin>:<action>` and are
|
||||
invoked via `extension/ui/action`; form submissions arrive via
|
||||
`extension/ui/submit`.
|
||||
|
||||
+34
-28
@@ -148,20 +148,20 @@ the CLI.
|
||||
|
||||
### Install Plugins
|
||||
|
||||
The installer has two modes:
|
||||
The installer takes one source:
|
||||
|
||||
- **Local folder**: click **Choose plugin folder** and select a plugin directory
|
||||
on disk. The selected path is shown next to the button.
|
||||
- **Git repository**: enter a Git source such as
|
||||
`git:github.com/obra/superpowers`. **Install name (optional)** can override
|
||||
the plugin manifest name for this install or overwrite.
|
||||
- **A repository address**, such as `https://github.com/obra/superpowers`, a
|
||||
branch, or a subdirectory of one.
|
||||
- **A local folder**: drag it onto the installer or click **选文件夹** (Choose
|
||||
folder).
|
||||
|
||||
Use the action buttons after choosing the source and options:
|
||||
A local `.zip` archive, such as one made by **Export**, installs through the CLI
|
||||
(`reasonix plugin install ./pkg.zip`) and the `install_source` tool.
|
||||
|
||||
- **Preview** validates the source and shows the planned install actions without
|
||||
writing files.
|
||||
- **Install plugin** installs the selected source using the current options.
|
||||
- **Refresh plugins** reloads the installed-plugin list from disk and config.
|
||||
Installing is always two steps: the installer first reads the source and shows
|
||||
what it will add and what it needs, without writing anything; **Install**
|
||||
then applies exactly that plan. A changed source between the two is refused
|
||||
rather than installed.
|
||||
|
||||
Installer options:
|
||||
|
||||
@@ -282,7 +282,7 @@ Native Reasonix extensions use the exact v2 `apiVersion`:
|
||||
"prompts": ["prompts"],
|
||||
"hooks": {},
|
||||
"mcpServers": {},
|
||||
"themes": ["themes/*.reasonix-theme"]
|
||||
"themes": ["themes/*/theme.json"]
|
||||
},
|
||||
"runtime": {
|
||||
"command": "${REASONIX_PLUGIN_ROOT}/bin/example",
|
||||
@@ -325,13 +325,16 @@ Parsing rules:
|
||||
New resource types:
|
||||
|
||||
- `prompts` are prompt templates using the same semantics and argument
|
||||
substitution as commands, invoked as `/<plugin>:<name>`. `commands` remains
|
||||
a compatible alias.
|
||||
- `themes` are `.reasonix-theme` files shown read-only in Desktop Settings as
|
||||
plugin themes (IDs `plugin:<plugin>:<theme>`); they are never copied into
|
||||
the user theme library. If the plugin is disabled or uninstalled while its
|
||||
theme is active, the desktop falls back to the base style but keeps the
|
||||
ID, so reinstalling the same plugin restores the theme.
|
||||
substitution as commands, invoked as `/<plugin>:<name>`. The two are
|
||||
discovered together; `commands` remains a compatible alias.
|
||||
- `themes` are Studio theme packs:
|
||||
- each entry names a pack's `theme.json`, as a path or a per-segment glob
|
||||
such as `themes/*/theme.json`; its `background` and `preview` images sit
|
||||
beside it;
|
||||
- a pack appears in appearance settings as `plugin:<plugin>:<directory>`;
|
||||
- packs are read in place, never copied into the user's library, and leave
|
||||
with the plugin;
|
||||
- any other file listed under `themes` is ignored.
|
||||
|
||||
The `runtime` block declares a code extension — a sidecar process Reasonix
|
||||
launches and talks to over the Extension Protocol (JSON-RPC 2.0 over stdio;
|
||||
@@ -521,14 +524,17 @@ Plugin hooks receive these environment variables:
|
||||
- `CLAUDE_PROJECT_DIR`
|
||||
- `CLAUDE_PLUGIN_ROOT`
|
||||
|
||||
## Desktop Backend Methods
|
||||
## Studio HTTP routes
|
||||
|
||||
Desktop exposes plugin package operations through Wails methods:
|
||||
Studio drives plugin packages through these kernel routes:
|
||||
|
||||
- `Plugins`
|
||||
- `PlanPluginInstall`
|
||||
- `InstallPlugin`
|
||||
- `RemovePlugin`
|
||||
- `SetPluginEnabled`
|
||||
- `UpdatePlugin`
|
||||
- `PluginDoctor`
|
||||
- `GET /plugins` lists installed packages and what each contributes
|
||||
- `POST /plugins/plan` reads a source and returns the install plan
|
||||
- `POST /plugins/install` applies a plan, echoing its `planId`; with `replace`
|
||||
set it is also the update path
|
||||
- `POST /plugins/enabled` enables or disables a package
|
||||
- `DELETE /plugins/{name}` removes a package
|
||||
- `GET /plugins/{name}/export` packs a package as a `.zip`, credentials stripped
|
||||
- `POST /extensions/reload` rebuilds the runtime from what is on disk
|
||||
|
||||
`reasonix plugin doctor` has no route; run it from the CLI.
|
||||
@@ -1,5 +1,13 @@
|
||||
# Reasonix Theme Pack V2
|
||||
|
||||
**Status:** this describes the retired Wails desktop and its `.reasonix-theme`
|
||||
archives. What Reasonix Studio reads instead:
|
||||
|
||||
- one directory per pack, holding a `theme.json` (`schemaVersion: 1`) and
|
||||
optional `background` and `preview` images, read by `internal/theme`;
|
||||
- plugin themes are packs of that shape, listed as
|
||||
`plugin:<plugin>:<directory>` (see `docs/PLUGIN_PACKAGES.md`).
|
||||
|
||||
Native theme packs for the Reasonix desktop app. Packs are controlled skins:
|
||||
semantic color tokens, density/corner recipes, and optional local images for
|
||||
the home and task/workspace scenes. They **cannot** run CSS, JavaScript, fonts,
|
||||
|
||||
@@ -120,11 +120,10 @@ func (c *Config) PluginPackageAgentOwners() map[string][]string {
|
||||
return out
|
||||
}
|
||||
|
||||
// pluginPackageCommandRoots returns the command directories contributed by
|
||||
// enabled installed plugin packages, in deterministic (name, path) order.
|
||||
// CommandRootsForRoot places them ahead of every user/project dir so explicit
|
||||
// commands win exact canonical-name clashes; LoadInstalled filters to enabled
|
||||
// packages.
|
||||
// pluginPackageCommandRoots returns the command and prompt directories of
|
||||
// enabled plugin packages in (name, path) order; both are invoked as
|
||||
// /<plugin>:<name>. CommandRootsForRoot places them ahead of every user and
|
||||
// project dir so explicit commands win exact canonical-name clashes.
|
||||
func (r Roots) pluginPackageCommandRoots() []command.Root {
|
||||
reasonixHome := r.Home()
|
||||
if strings.TrimSpace(reasonixHome) == "" {
|
||||
@@ -133,7 +132,7 @@ func (r Roots) pluginPackageCommandRoots() []command.Root {
|
||||
installed, _ := pluginpkg.LoadInstalled(reasonixHome)
|
||||
var out []command.Root
|
||||
for _, item := range installed {
|
||||
for _, root := range item.Package.CommandRoots() {
|
||||
for _, root := range append(item.Package.CommandRoots(), item.Package.PromptRoots()...) {
|
||||
out = append(out, command.Root{Path: root, Plugin: item.Installed.Name})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reasonix/internal/command"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/pluginpkg"
|
||||
@@ -186,3 +187,33 @@ func TestCommandDirsWithoutPluginState(t *testing.T) {
|
||||
t.Fatal("CommandDirsForRoot must still return the conventional dirs")
|
||||
}
|
||||
}
|
||||
|
||||
// A package's prompts are invoked the way its commands are: /<plugin>:<name>.
|
||||
func TestPluginPromptsAreInvokedLikeItsCommands(t *testing.T) {
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
root := filepath.Join(home, "plugins", "review")
|
||||
writeConfigTestFile(t, filepath.Join(root, pluginpkg.NativeManifest), `{
|
||||
"apiVersion": "reasonix.io/plugin/v2",
|
||||
"name": "review",
|
||||
"version": "1.0.0",
|
||||
"contributes": {"prompts": ["prompts"]}
|
||||
}`)
|
||||
writeConfigTestFile(t, filepath.Join(root, "prompts", "diff.md"), `---
|
||||
description: review the diff
|
||||
---
|
||||
Review $ARGUMENTS`)
|
||||
if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{Name: "review", Root: "plugins/review", Version: "1.0.0", ManifestKind: "reasonix", Enabled: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmds, err := command.LoadRoots(CommandRootsForRoot(testenv.TempDir(t))...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cmds {
|
||||
if c.Name == "review:diff" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("no /review:diff among %d commands", len(cmds))
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
// lives in the existing packages (internal/skill, internal/command,
|
||||
// internal/hook, internal/tool, internal/plugin, internal/provider), which
|
||||
// run it and hand the kernel the resolved records; adapters.go maps one such
|
||||
// record onto one Contribution. KindPrompt and KindTheme are declared in the
|
||||
// catalog but have no contributor: a plugin package's prompts and themes are
|
||||
// parsed by pluginpkg and reach no snapshot. Winner rules are resolved here,
|
||||
// record onto one Contribution. KindPrompt and KindTheme have no contributor:
|
||||
// a package's prompts are found with its commands, and internal/theme reads
|
||||
// its themes. Winner rules are resolved here,
|
||||
// once, identically for every caller: a higher-tier scope shadows a lower one
|
||||
// for the same canonical ID, same-tier duplicates from different sources are
|
||||
// hard conflicts instead of silent overrides, and hooks/interceptors stay
|
||||
|
||||
@@ -99,7 +99,7 @@ type CommandRef struct {
|
||||
// PromptRef is one prompt template a v2 plugin contributes from a prompts
|
||||
// directory (distinct from legacy command contributions).
|
||||
// Prompt files share the slash-command file shape (flat <name>.md with
|
||||
// frontmatter) but map to kernel KindPrompt contributions, not commands.
|
||||
// frontmatter) and are discovered with the package's commands.
|
||||
type PromptRef struct {
|
||||
Name string
|
||||
Description string
|
||||
@@ -151,9 +151,9 @@ type Manifest struct {
|
||||
Commands []string
|
||||
Hooks map[string][]Hook
|
||||
MCPServers map[string]MCPServer
|
||||
// Prompts are directories of flat <name>.md prompt templates. The two are
|
||||
// separate semantic sets: commands become slash commands, prompts become
|
||||
// kernel KindPrompt contributions. A path listed under both stays in both.
|
||||
// Prompts are directories of flat <name>.md prompt templates, invoked the
|
||||
// same way as commands: /<plugin>:<name>. A path listed under both is read
|
||||
// once.
|
||||
Prompts []string
|
||||
// Themes are *.reasonix-theme file paths or per-segment glob patterns
|
||||
// (e.g. "themes/*.reasonix-theme"), all lexically inside the plugin root.
|
||||
@@ -894,7 +894,7 @@ func (p Package) AgentCount() int { return len(p.agentRefs()) }
|
||||
func (p Package) PromptCount() int { return len(p.promptRefs()) }
|
||||
|
||||
// ThemeCount counts the theme files resolved from Manifest.Themes.
|
||||
func (p Package) ThemeCount() int { return len(p.themeRefs()) }
|
||||
func (p Package) ThemeCount() int { return len(p.ThemeFiles()) }
|
||||
|
||||
// CapabilitySummary is the full per-package capability count set. The
|
||||
// four-value CapabilityCounts predates native runtime manifests and keeps its
|
||||
@@ -922,7 +922,7 @@ func (p Package) CapabilitySummary() CapabilitySummary {
|
||||
Hooks: hooks,
|
||||
MCPServers: mcp,
|
||||
Prompts: len(p.promptRefs()),
|
||||
Themes: len(p.themeRefs()),
|
||||
Themes: len(p.ThemeFiles()),
|
||||
Runtime: p.Manifest.Runtime != nil,
|
||||
}
|
||||
}
|
||||
@@ -933,7 +933,7 @@ func (p Package) Inventory() Inventory {
|
||||
Agents: p.agentRefs(),
|
||||
Commands: p.commandRefs(),
|
||||
Prompts: p.promptRefs(),
|
||||
Themes: p.themeRefs(),
|
||||
Themes: p.ThemeFiles(),
|
||||
Hooks: p.hookRefs(),
|
||||
MCPServers: p.mcpServerRefs(),
|
||||
}
|
||||
@@ -983,11 +983,11 @@ func (p Package) promptRefs() []PromptRef {
|
||||
return out
|
||||
}
|
||||
|
||||
// themeRefs resolves the manifest's themes list (plain paths and
|
||||
// ThemeFiles resolves the manifest's themes list (plain paths and
|
||||
// per-segment globs) to concrete theme files. Parse-time validation has
|
||||
// already rejected escapes and non-regular files, so unreadable entries
|
||||
// here simply drop out (they were reported as parse warnings).
|
||||
func (p Package) themeRefs() []ThemeRef {
|
||||
func (p Package) ThemeFiles() []ThemeRef {
|
||||
seen := map[string]bool{}
|
||||
var out []ThemeRef
|
||||
for _, pattern := range p.Manifest.Themes {
|
||||
|
||||
+16
-2
@@ -55,8 +55,12 @@ func Asset(id string, kind Kind) ([]byte, string, error) {
|
||||
if err := validID(id); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
dir, local := assetDir(id)
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(Dir(), id, string(kind)+ext)
|
||||
if !local {
|
||||
break
|
||||
}
|
||||
path := filepath.Join(dir, string(kind)+ext)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
@@ -71,6 +75,9 @@ func Asset(id string, kind Kind) ([]byte, string, error) {
|
||||
return raw, contentTypes[ext], nil
|
||||
}
|
||||
for _, ext := range extensions {
|
||||
if isPluginID(id) {
|
||||
break
|
||||
}
|
||||
raw, err := builtin.ReadFile(builtinAssetName(id, kind, ext))
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -84,10 +91,17 @@ func hasAsset(id string, kind Kind) bool {
|
||||
if validID(id) != nil {
|
||||
return false
|
||||
}
|
||||
dir, local := assetDir(id)
|
||||
for _, ext := range extensions {
|
||||
if info, err := os.Stat(filepath.Join(Dir(), id, string(kind)+ext)); err == nil && !info.IsDir() {
|
||||
if !local {
|
||||
break
|
||||
}
|
||||
if info, err := os.Stat(filepath.Join(dir, string(kind)+ext)); err == nil && !info.IsDir() {
|
||||
return true
|
||||
}
|
||||
if isPluginID(id) {
|
||||
continue
|
||||
}
|
||||
if _, err := fs.Stat(builtin, builtinAssetName(id, kind, ext)); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"reasonix/internal/config"
|
||||
"reasonix/internal/pluginpkg"
|
||||
)
|
||||
|
||||
// A pack an enabled plugin package contributes is read in place from the
|
||||
// plugin's root, never copied into the user's library. Its id names the plugin
|
||||
// and the pack's directory, so it can shadow neither a shipped nor a user pack,
|
||||
// and it goes away with the plugin rather than lingering after an uninstall.
|
||||
const pluginPrefix = "plugin:"
|
||||
|
||||
type pluginPack struct {
|
||||
id string
|
||||
dir string
|
||||
}
|
||||
|
||||
func isPluginID(id string) bool { return strings.HasPrefix(id, pluginPrefix) }
|
||||
|
||||
// pluginPacks lists every theme.json an enabled plugin declares under
|
||||
// contributes.themes. Other theme files are not packs this reader knows.
|
||||
func pluginPacks() []pluginPack {
|
||||
installed, _ := pluginpkg.LoadInstalled(config.RootDir(config.RootHome))
|
||||
var out []pluginPack
|
||||
for _, item := range installed {
|
||||
for _, ref := range item.Package.ThemeFiles() {
|
||||
if filepath.Base(ref.Path) != manifestName {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Dir(ref.Path)
|
||||
out = append(out, pluginPack{id: pluginPrefix + item.Installed.Name + ":" + filepath.Base(dir), dir: dir})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pluginDir(id string) (string, bool) {
|
||||
for _, p := range pluginPacks() {
|
||||
if p.id == id {
|
||||
return p.dir, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// assetDir is where a pack's images live on disk. A plugin id is never joined
|
||||
// onto the user library path: its colon is not a file name on every system.
|
||||
func assetDir(id string) (string, bool) {
|
||||
if isPluginID(id) {
|
||||
return pluginDir(id)
|
||||
}
|
||||
return filepath.Join(Dir(), id), true
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/pluginpkg"
|
||||
"reasonix/internal/testenv"
|
||||
)
|
||||
|
||||
func installThemePlugin(t *testing.T, home string, enabled bool) string {
|
||||
t.Helper()
|
||||
root := filepath.Join(home, "plugins", "neon")
|
||||
writePack(t, filepath.Join(root, "themes"), "night", minimal)
|
||||
if err := os.WriteFile(filepath.Join(root, "themes", "night", "background.png"), []byte("png"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"apiVersion":"reasonix.io/plugin/v2","name":"neon","version":"1.0.0","contributes":{"themes":["themes/*/theme.json"]}}`
|
||||
if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{Name: "neon", Root: "plugins/neon", Version: "1.0.0", ManifestKind: "reasonix", Enabled: enabled}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// A plugin's pack is read in place under a name that says whose it is, and it
|
||||
// serves its own images; it is never copied into the user's library.
|
||||
func TestAnEnabledPluginContributesAPackUnderItsOwnName(t *testing.T) {
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
installThemePlugin(t, home, true)
|
||||
|
||||
const id = "plugin:neon:night"
|
||||
var listed bool
|
||||
for _, p := range List() {
|
||||
listed = listed || p.ID == id
|
||||
}
|
||||
if !listed {
|
||||
t.Fatalf("List() lacks %s: %+v", id, List())
|
||||
}
|
||||
pack, err := Load(id)
|
||||
if err != nil || pack.Name != "Dusk" || pack.Background == nil || !pack.Background.Image {
|
||||
t.Fatalf("Load = %+v, %v; want the pack with its background", pack, err)
|
||||
}
|
||||
if raw, _, err := Asset(id, assetBackground); err != nil || string(raw) != "png" {
|
||||
t.Fatalf("Asset = %q, %v", raw, err)
|
||||
}
|
||||
if entries, _ := os.ReadDir(Dir()); len(entries) != 0 {
|
||||
t.Fatalf("the user library gained %d entries", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestADisabledPluginContributesNoPack(t *testing.T) {
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
installThemePlugin(t, home, false)
|
||||
if _, err := Load("plugin:neon:night"); err == nil {
|
||||
t.Fatal("a disabled plugin's pack loaded")
|
||||
}
|
||||
if _, _, err := Asset("plugin:neon:night", assetBackground); err == nil {
|
||||
t.Fatal("a disabled plugin's image was served")
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,11 @@ func List() []Pack {
|
||||
byID[pack.ID] = pack
|
||||
}
|
||||
}
|
||||
for _, p := range pluginPacks() {
|
||||
if pack, err := load(filepath.Join(p.dir, manifestName), p.id); err == nil {
|
||||
byID[pack.ID] = pack
|
||||
}
|
||||
}
|
||||
out := make([]Pack, 0, len(byID))
|
||||
for _, pack := range byID {
|
||||
out = append(out, pack)
|
||||
@@ -178,6 +183,13 @@ func Load(id string) (Pack, error) {
|
||||
if err := validID(id); err != nil {
|
||||
return Pack{}, err
|
||||
}
|
||||
if isPluginID(id) {
|
||||
dir, ok := pluginDir(id)
|
||||
if !ok {
|
||||
return Pack{}, fmt.Errorf("theme: no enabled plugin contributes %q", id)
|
||||
}
|
||||
return load(filepath.Join(dir, manifestName), id)
|
||||
}
|
||||
if pack, err := load(filepath.Join(Dir(), id, manifestName), id); err == nil {
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"complexity": 1009,
|
||||
"doc-language": 0,
|
||||
"doc-owner": 43,
|
||||
"doc-prose": 2326,
|
||||
"doc-prose": 2325,
|
||||
"error-text": 9,
|
||||
"essay": 2907,
|
||||
"file-size": 24032,
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
"docs/PLUGIN_PACKAGES.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 103
|
||||
"doc-prose": 102
|
||||
},
|
||||
"docs/REASONING_LANGUAGE.md": {
|
||||
"doc-owner": 1,
|
||||
|
||||
Reference in new issue
Block a user