@north-light/crouter 0.3.183 → 0.3.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +10 -3
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/clients/attach/input/capabilities.js +3 -0
- package/dist/clients/attach/slash/dispatch.js +12 -0
- package/dist/clients/attach/viewer.js +191 -191
- package/dist/commands/pkg/browse/actions.d.ts +7 -0
- package/dist/commands/pkg/browse/actions.js +199 -0
- package/dist/commands/pkg/browse/catalog.d.ts +9 -0
- package/dist/commands/pkg/browse/catalog.js +466 -0
- package/dist/commands/pkg/browse/command-view.d.ts +3 -0
- package/dist/commands/pkg/browse/command-view.js +191 -0
- package/dist/commands/pkg/browse/detail-panel.d.ts +66 -0
- package/dist/commands/pkg/browse/detail-panel.js +402 -0
- package/dist/commands/pkg/browse/doc-view.d.ts +3 -0
- package/dist/commands/pkg/browse/doc-view.js +81 -0
- package/dist/commands/pkg/browse/list-panel.d.ts +28 -0
- package/dist/commands/pkg/browse/list-panel.js +168 -0
- package/dist/commands/pkg/browse/model.d.ts +230 -0
- package/dist/commands/pkg/browse/model.js +36 -0
- package/dist/commands/pkg/browse/shell.d.ts +93 -0
- package/dist/commands/pkg/browse/shell.js +640 -0
- package/dist/commands/pkg/browse.d.ts +2 -0
- package/dist/commands/pkg/browse.js +27 -0
- package/dist/commands/pkg/market-manage.js +13 -4
- package/dist/commands/pkg/plugin-manage.js +8 -3
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/sys/doctor.js +6 -4
- package/dist/commands/sys/panels/broker-limits-panel.d.ts +1 -1
- package/dist/commands/sys/panels/broker-limits-panel.js +1 -1
- package/dist/commands/sys/panels/dependencies-panel.d.ts +1 -1
- package/dist/commands/sys/panels/dependencies-panel.js +1 -1
- package/dist/commands/sys/panels/keybindings-panel.js +1 -1
- package/dist/commands/sys/panels/models-panel.d.ts +1 -1
- package/dist/commands/sys/panels/models-panel.js +1 -1
- package/dist/commands/sys/panels/plugins-panel.d.ts +1 -1
- package/dist/commands/sys/panels/plugins-panel.js +1 -1
- package/dist/commands/sys/panels/prefs-panel.d.ts +1 -1
- package/dist/commands/sys/panels/prefs-panel.js +1 -1
- package/dist/commands/sys/panels/provider-panel.d.ts +1 -1
- package/dist/commands/sys/panels/provider-panel.js +1 -1
- package/dist/commands/sys/settings-shell.js +1 -1
- package/dist/commands/sys/setup-settings-runner.d.ts +7 -18
- package/dist/commands/sys/setup-settings-runner.js +3 -52
- package/dist/commands/sys/setup-wizard.js +1 -1
- package/dist/core/__tests__/serial/command-plugins.test.js +21 -40
- package/dist/core/canvas/nav-model.d.ts +1 -8
- package/dist/core/canvas/nav-model.js +30 -59
- package/dist/core/canvas/nav-render.d.ts +2 -2
- package/dist/core/canvas/nav-render.js +8 -6
- package/dist/core/command-manifests/registry.d.ts +10 -3
- package/dist/core/command-manifests/registry.js +36 -22
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/discovery.js +15 -7
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/resolver.d.ts +2 -2
- package/dist/core/runtime/tmux-bindings.js +2 -0
- package/dist/core/tui/page-host.d.ts +25 -0
- package/dist/core/tui/page-host.js +65 -0
- package/dist/daemon/__tests__/subtree-filter-freshness.test.d.ts +1 -0
- package/dist/daemon/__tests__/subtree-filter-freshness.test.js +63 -0
- package/dist/pi-extensions/canvas-bash-valve.js +45 -1
- package/dist/types.d.ts +4 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- /package/dist/{commands/sys/panels → core/tui}/panel.d.ts +0 -0
- /package/dist/{commands/sys/panels → core/tui}/panel.js +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { padAnsi, theme, visibleRange } from "../../../core/tui/panel.js";
|
|
3
|
+
import { displayVersion, plural } from "./model.js";
|
|
4
|
+
/** The middle column of the plugin browser. It owns filtering and row position;
|
|
5
|
+
* the shell owns horizontal navigation and actions. */
|
|
6
|
+
export class PkgListPanel {
|
|
7
|
+
onChange;
|
|
8
|
+
source;
|
|
9
|
+
index = 0;
|
|
10
|
+
filter = "";
|
|
11
|
+
filtering = false;
|
|
12
|
+
notice;
|
|
13
|
+
hasMarketplaces = false;
|
|
14
|
+
constructor(onChange) {
|
|
15
|
+
this.onChange = onChange;
|
|
16
|
+
}
|
|
17
|
+
get selected() {
|
|
18
|
+
const entries = this.entries();
|
|
19
|
+
this.index = Math.max(0, Math.min(this.index, entries.length - 1));
|
|
20
|
+
return entries[this.index];
|
|
21
|
+
}
|
|
22
|
+
get ownsKeyboard() {
|
|
23
|
+
return this.filtering;
|
|
24
|
+
}
|
|
25
|
+
setSource(source, selectedKey) {
|
|
26
|
+
const changed = this.source?.id !== source.id;
|
|
27
|
+
this.source = source;
|
|
28
|
+
const entries = this.entries();
|
|
29
|
+
const found = selectedKey === undefined ? -1 : entries.findIndex((entry) => entry.key === selectedKey);
|
|
30
|
+
this.index = found >= 0 ? found : changed ? 0 : Math.min(this.index, Math.max(0, entries.length - 1));
|
|
31
|
+
if (changed) {
|
|
32
|
+
this.filter = "";
|
|
33
|
+
this.filtering = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
setEmptyState(hasMarketplaces) {
|
|
37
|
+
this.hasMarketplaces = hasMarketplaces;
|
|
38
|
+
}
|
|
39
|
+
move(delta) {
|
|
40
|
+
const entries = this.entries();
|
|
41
|
+
if (entries.length === 0)
|
|
42
|
+
return;
|
|
43
|
+
this.index = Math.max(0, Math.min(entries.length - 1, this.index + delta));
|
|
44
|
+
this.onChange();
|
|
45
|
+
}
|
|
46
|
+
/** Returns true only when a live filter consumed the input. */
|
|
47
|
+
handleInput(data) {
|
|
48
|
+
if (this.filtering) {
|
|
49
|
+
if (matchesKey(data, "escape")) {
|
|
50
|
+
this.filter = "";
|
|
51
|
+
this.filtering = false;
|
|
52
|
+
}
|
|
53
|
+
else if (matchesKey(data, "enter")) {
|
|
54
|
+
// Enter leaves entry mode but deliberately keeps the narrowed list.
|
|
55
|
+
this.filtering = false;
|
|
56
|
+
}
|
|
57
|
+
else if (matchesKey(data, "backspace")) {
|
|
58
|
+
this.filter = this.filter.slice(0, -1);
|
|
59
|
+
this.index = 0;
|
|
60
|
+
}
|
|
61
|
+
else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
62
|
+
this.filter += data;
|
|
63
|
+
this.index = 0;
|
|
64
|
+
}
|
|
65
|
+
this.onChange();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (data === "/" || matchesKey(data, "/")) {
|
|
69
|
+
this.filtering = true;
|
|
70
|
+
this.onChange();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Escape has a useful local meaning only while a committed filter remains.
|
|
74
|
+
if (matchesKey(data, "escape") && this.filter !== "") {
|
|
75
|
+
this.filter = "";
|
|
76
|
+
this.index = 0;
|
|
77
|
+
this.onChange();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
setNotice(notice) {
|
|
83
|
+
this.notice = notice;
|
|
84
|
+
}
|
|
85
|
+
entries() {
|
|
86
|
+
const entries = this.source?.entries ?? [];
|
|
87
|
+
const needle = this.filter.trim().toLowerCase();
|
|
88
|
+
if (needle === "")
|
|
89
|
+
return entries;
|
|
90
|
+
return entries.filter((entry) => [entry.name, entry.description ?? "", ...entry.keywords]
|
|
91
|
+
.join("\n")
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.includes(needle));
|
|
94
|
+
}
|
|
95
|
+
render(width, height, focused) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const source = this.source;
|
|
98
|
+
const entries = this.entries();
|
|
99
|
+
const sourceLabel = source?.kind === "installed" ? "Installed" : source?.label ?? "Library";
|
|
100
|
+
const filterLabel = this.filtering
|
|
101
|
+
? ` /${this.filter} · ${plural(entries.length, "match", "matches")}`
|
|
102
|
+
: this.filter === ""
|
|
103
|
+
? ` ${plural(entries.length, "plugin", "plugins")}`
|
|
104
|
+
: ` ${plural(entries.length, "match", "matches")} · esc clears`;
|
|
105
|
+
// A marketplace name can be wider than this whole column, so the source
|
|
106
|
+
// label yields first: how many rows matched and how to get back out are
|
|
107
|
+
// what a filtering user needs, and truncating the pair together drops
|
|
108
|
+
// exactly that half off the end.
|
|
109
|
+
const labelRoom = Math.max(3, width - visibleWidth(filterLabel));
|
|
110
|
+
out.push(truncateToWidth(`${theme.fg("accent", theme.bold(truncateToWidth(sourceLabel, labelRoom)))}${theme.fg("dim", filterLabel)}`, width));
|
|
111
|
+
if (this.notice !== undefined) {
|
|
112
|
+
out.push(truncateToWidth(theme.fg("warning", `! ${this.notice}`), width));
|
|
113
|
+
}
|
|
114
|
+
if (entries.length === 0) {
|
|
115
|
+
out.push("");
|
|
116
|
+
const firstRun = source?.kind === "installed" && !this.hasMarketplaces;
|
|
117
|
+
if (firstRun) {
|
|
118
|
+
out.push(truncateToWidth(theme.fg("accent", "No marketplaces yet."), width));
|
|
119
|
+
out.push(truncateToWidth(theme.fg("dim", "Press a to add a marketplace."), width));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
out.push(truncateToWidth(theme.fg("dim", "No plugins installed yet."), width));
|
|
123
|
+
out.push(truncateToWidth(theme.fg("dim", "Press tab to browse marketplaces."), width));
|
|
124
|
+
}
|
|
125
|
+
return this.fit(out, height);
|
|
126
|
+
}
|
|
127
|
+
const available = Math.max(1, height - out.length);
|
|
128
|
+
const range = visibleRange(entries.length, this.index, available);
|
|
129
|
+
for (let position = range.start; position < range.end; position += 1) {
|
|
130
|
+
const entry = entries[position];
|
|
131
|
+
const selected = position === this.index;
|
|
132
|
+
const glyph = this.stateGlyph(entry);
|
|
133
|
+
const version = displayVersion(entry);
|
|
134
|
+
const prefix = `${glyph} ${entry.name}`;
|
|
135
|
+
const suffix = version === undefined ? "" : ` ${version}`;
|
|
136
|
+
const fixed = visibleWidth(prefix) + visibleWidth(suffix);
|
|
137
|
+
const descriptionWidth = width - fixed - 2;
|
|
138
|
+
const description = descriptionWidth >= 12 && entry.description
|
|
139
|
+
? theme.fg("dim", ` ${truncateToWidth(entry.description, descriptionWidth)}`)
|
|
140
|
+
: "";
|
|
141
|
+
const row = truncateToWidth(`${prefix}${suffix}${description}`, width);
|
|
142
|
+
out.push(selected && focused
|
|
143
|
+
? theme.bg("selectedBg", padAnsi(row, width))
|
|
144
|
+
: selected
|
|
145
|
+
? `${theme.fg("accent", "▌")}${truncateToWidth(theme.fg("accent", row), Math.max(0, width - 1))}`
|
|
146
|
+
: row);
|
|
147
|
+
}
|
|
148
|
+
return this.fit(out, height);
|
|
149
|
+
}
|
|
150
|
+
/** Pad rather than trim: this column sets the length of the divider drawn
|
|
151
|
+
* beside it, and a divider that stops where the content runs out makes the
|
|
152
|
+
* page look half-drawn. */
|
|
153
|
+
fit(lines, height) {
|
|
154
|
+
while (lines.length < height)
|
|
155
|
+
lines.push("");
|
|
156
|
+
return lines.slice(0, height);
|
|
157
|
+
}
|
|
158
|
+
stateGlyph(entry) {
|
|
159
|
+
if (entry.install.kind === "absent")
|
|
160
|
+
return " ";
|
|
161
|
+
if (entry.install.scope === "builtin")
|
|
162
|
+
return theme.fg("muted", "⌂");
|
|
163
|
+
if (entry.install.enabled) {
|
|
164
|
+
return entry.install.updateAvailable ? theme.fg("warning", "↑") : theme.fg("success", "✓");
|
|
165
|
+
}
|
|
166
|
+
return theme.fg("dim", "○");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Field, InputParam } from "../../../core/help.js";
|
|
2
|
+
import type { Rung } from "../../../core/substrate/schema.js";
|
|
3
|
+
import type { OwnerRef, Scope } from "../../../types.js";
|
|
4
|
+
/** The scopes that can hold a marketplace, and that a plugin can be installed
|
|
5
|
+
* into. Narrower than `Scope`: `builtin` names plugins that ship with crtr, and
|
|
6
|
+
* `listAllMarketplaces()` never yields it, so a builtin marketplace or a
|
|
7
|
+
* builtin install target is not a state this surface can reach. */
|
|
8
|
+
export type WritableScope = Exclude<Scope, "builtin">;
|
|
9
|
+
/** Rail row identity: `installed`, or `market:<scope>:<name>`.
|
|
10
|
+
*
|
|
11
|
+
* A marketplace id carries its scope because the same name can be registered
|
|
12
|
+
* in both the user and the project scope. Collapsing those to one id makes two
|
|
13
|
+
* distinct marketplaces share a row, and an action on it reach whichever one
|
|
14
|
+
* the leaf's unscoped lookup resolves first — including a destructive one. */
|
|
15
|
+
export type SourceId = string;
|
|
16
|
+
export declare const INSTALLED_SOURCE_ID: SourceId;
|
|
17
|
+
export declare function marketplaceSourceId(name: string, scope: WritableScope): SourceId;
|
|
18
|
+
export type PluginInstallState = {
|
|
19
|
+
kind: "absent";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "installed";
|
|
22
|
+
scope: Scope;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
/** Version recorded for the installed copy, when it declares one. */
|
|
25
|
+
version?: string;
|
|
26
|
+
path: string;
|
|
27
|
+
/** The marketplace offers a version newer than the installed one. */
|
|
28
|
+
updateAvailable: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** One command node a plugin contributes to the `crtr` tree, with the whole
|
|
31
|
+
* contract an agent would read from `-h`. The browser renders the tree, so
|
|
32
|
+
* children hang off their parent rather than arriving pre-flattened. */
|
|
33
|
+
export interface CommandNode {
|
|
34
|
+
/** Path words from the crtr root, e.g. `['search', 'web']`. */
|
|
35
|
+
path: string[];
|
|
36
|
+
kind: "branch" | "leaf";
|
|
37
|
+
description: string;
|
|
38
|
+
/** The selection rubric — when to reach for this command. */
|
|
39
|
+
whenToUse: string;
|
|
40
|
+
summary: string;
|
|
41
|
+
tier?: "normal" | "common" | "important";
|
|
42
|
+
/** Empty for a leaf and for a passthrough branch. */
|
|
43
|
+
children: CommandNode[];
|
|
44
|
+
/** Branch only: forwards every argv token to an external binary instead of
|
|
45
|
+
* parsing children. */
|
|
46
|
+
passthrough?: {
|
|
47
|
+
bin: string;
|
|
48
|
+
installHint: string;
|
|
49
|
+
};
|
|
50
|
+
/** Top-level branch only: its entry in the crtr root listing. */
|
|
51
|
+
rootEntry?: {
|
|
52
|
+
concept: string;
|
|
53
|
+
description: string;
|
|
54
|
+
whenToUse: string;
|
|
55
|
+
};
|
|
56
|
+
/** Leaf only: the exact invocation schema, straight from the manifest. */
|
|
57
|
+
params: InputParam[];
|
|
58
|
+
output: Field[];
|
|
59
|
+
effects: string[];
|
|
60
|
+
/** Leaf on an http-transport plugin only: the REST call it maps to. */
|
|
61
|
+
rest?: {
|
|
62
|
+
method: string;
|
|
63
|
+
path: string;
|
|
64
|
+
streaming: boolean;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** `crtr search web` — how a user types the command a node names. The one place
|
|
68
|
+
* a command path becomes a label, so no corner of the page invents its own. */
|
|
69
|
+
export declare function commandLabel(node: CommandNode): string;
|
|
70
|
+
/** Every node in a command forest, parents before their children. */
|
|
71
|
+
export declare function flattenCommands(nodes: CommandNode[]): CommandNode[];
|
|
72
|
+
/** One memory doc a plugin provides — whole, because the resolver already read
|
|
73
|
+
* the file to report the doc exists, so the body costs nothing extra.
|
|
74
|
+
*
|
|
75
|
+
* Two fields sound alike and are not: `shortForm` is the human-facing hook —
|
|
76
|
+
* what this doc SAYS — and is the description the page shows beside a doc's
|
|
77
|
+
* name. `routingLine` is agent read-routing — WHEN to open it — and belongs
|
|
78
|
+
* under the doc view's own heading, never in the description slot. */
|
|
79
|
+
export interface DocDetail {
|
|
80
|
+
/** `<plugin>/<path>` — the identifier `crtr memory read` takes. */
|
|
81
|
+
name: string;
|
|
82
|
+
kind: "knowledge" | "preference";
|
|
83
|
+
/** Frontmatter `short-form`: the abbreviated content. */
|
|
84
|
+
shortForm?: string;
|
|
85
|
+
/** Frontmatter `when-and-why-to-read`: one sentence routing a reading agent. */
|
|
86
|
+
routingLine?: string;
|
|
87
|
+
/** Absolute path to the `.md` on disk. */
|
|
88
|
+
path: string;
|
|
89
|
+
sizeBytes?: number;
|
|
90
|
+
/** How much of the doc surfaces at boot, and on a related file read. */
|
|
91
|
+
systemPromptVisibility: Rung;
|
|
92
|
+
fileReadVisibility: Rung;
|
|
93
|
+
/** Invocable as a pi slash command (`slash: true`). */
|
|
94
|
+
slash: boolean;
|
|
95
|
+
/** Frontmatter `applies-to`: explicit file-context routes. */
|
|
96
|
+
appliesTo?: string[];
|
|
97
|
+
/** Frontmatter `gate`: eligibility predicate over the node's config. */
|
|
98
|
+
gate?: Record<string, unknown>;
|
|
99
|
+
/** Frontmatter `read-when`: predicate over a READ FILE's own frontmatter.
|
|
100
|
+
* Unrelated to `routingLine` despite the neighbouring names. */
|
|
101
|
+
readWhenPredicate?: Record<string, unknown>;
|
|
102
|
+
/** Frontmatter `rationale`: the observed failure the doc exists to close.
|
|
103
|
+
* Maintainer-facing — never delivered to a reading agent. */
|
|
104
|
+
rationale?: string;
|
|
105
|
+
/** Markdown body, frontmatter stripped. */
|
|
106
|
+
body: string;
|
|
107
|
+
}
|
|
108
|
+
/** A plugin as one list row: everything cheap enough to load for every row. */
|
|
109
|
+
export interface PluginEntry {
|
|
110
|
+
/** Stable row key, unique across the whole catalog: `<sourceId>:<scope>:<name>`.
|
|
111
|
+
* Scope participates because one name can name two different installations. */
|
|
112
|
+
key: string;
|
|
113
|
+
name: string;
|
|
114
|
+
/** What `pkg plugin install` takes: `<marketplace>/<plugin>`, else the name. */
|
|
115
|
+
ref: string;
|
|
116
|
+
sourceId: SourceId;
|
|
117
|
+
/** The marketplace offering it; absent for a plugin installed from a URL or path. */
|
|
118
|
+
marketplace?: string;
|
|
119
|
+
/** Version the marketplace offers (installed version lives in `install`). */
|
|
120
|
+
version?: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
keywords: string[];
|
|
123
|
+
owner?: OwnerRef;
|
|
124
|
+
/** Upstream repo the marketplace entry points at. */
|
|
125
|
+
source?: string;
|
|
126
|
+
install: PluginInstallState;
|
|
127
|
+
/** Directory holding the plugin's files — the installed copy when there is
|
|
128
|
+
* one, else the marketplace clone's vendored copy. Absent when neither
|
|
129
|
+
* exists on disk, which is the only case where capability is unknowable. */
|
|
130
|
+
root?: string;
|
|
131
|
+
}
|
|
132
|
+
/** The version to show beside a plugin's name, anywhere in the page.
|
|
133
|
+
*
|
|
134
|
+
* One rule, one place: the version beside a name is always the one in play on
|
|
135
|
+
* THIS machine — what is installed, or what installing would get you. The
|
|
136
|
+
* marketplace's offer for an already-installed plugin is a different fact, and
|
|
137
|
+
* it appears only as the right-hand side of the update arrow in the status
|
|
138
|
+
* line. Headline and list row both call this so they cannot disagree. */
|
|
139
|
+
export declare function displayVersion(entry: PluginEntry): string | undefined;
|
|
140
|
+
/** `<count> <noun>`, with the noun agreeing with the count. Every count this
|
|
141
|
+
* page renders goes through here so no corner of it reads "1 docs". */
|
|
142
|
+
export declare function plural(count: number, one: string, many: string): string;
|
|
143
|
+
/** What a plugin adds to this machine — read from its directory on demand. */
|
|
144
|
+
export interface PluginCapability {
|
|
145
|
+
/** Directory these facts were read from. */
|
|
146
|
+
root: string;
|
|
147
|
+
/** How crtr invokes its commands; null for a docs-only plugin. */
|
|
148
|
+
transport: "exec" | "http" | null;
|
|
149
|
+
/** Local executable (exec) or remote endpoint (http) — the trust-relevant fact. */
|
|
150
|
+
transportTarget?: string;
|
|
151
|
+
/** The command forest this plugin mounts, as a tree. */
|
|
152
|
+
commands: CommandNode[];
|
|
153
|
+
/** Static validation problems that stop a declared command from mounting. */
|
|
154
|
+
commandIssues: {
|
|
155
|
+
message: string;
|
|
156
|
+
next?: string;
|
|
157
|
+
}[];
|
|
158
|
+
docs: DocDetail[];
|
|
159
|
+
/** Total bytes on disk, when cheap to measure. */
|
|
160
|
+
sizeBytes?: number;
|
|
161
|
+
}
|
|
162
|
+
export interface MarketplaceDetail {
|
|
163
|
+
name: string;
|
|
164
|
+
scope: WritableScope;
|
|
165
|
+
url: string;
|
|
166
|
+
ref: string;
|
|
167
|
+
/** Absolute path to the clone. */
|
|
168
|
+
path: string;
|
|
169
|
+
owner?: OwnerRef;
|
|
170
|
+
version?: string;
|
|
171
|
+
pluginCount: number;
|
|
172
|
+
installedCount: number;
|
|
173
|
+
/** Installed plugins from this marketplace with a newer version offered. */
|
|
174
|
+
updatableCount: number;
|
|
175
|
+
/** Short sha of the clone's checked-out commit. */
|
|
176
|
+
revision?: string;
|
|
177
|
+
/** ISO date of that commit. */
|
|
178
|
+
revisionDate?: string;
|
|
179
|
+
}
|
|
180
|
+
export interface CatalogSource {
|
|
181
|
+
id: SourceId;
|
|
182
|
+
kind: "installed" | "marketplace";
|
|
183
|
+
label: string;
|
|
184
|
+
/** Present exactly when `kind === 'marketplace'`. */
|
|
185
|
+
marketplace?: MarketplaceDetail;
|
|
186
|
+
entries: PluginEntry[];
|
|
187
|
+
}
|
|
188
|
+
export interface Catalog {
|
|
189
|
+
sources: CatalogSource[];
|
|
190
|
+
/** Scopes a plugin can be installed into on this machine, in offer order. */
|
|
191
|
+
installScopes: WritableScope[];
|
|
192
|
+
/** Non-fatal problems loading the catalog — a broken clone, an unreadable
|
|
193
|
+
* manifest. Surfaced in the page rather than thrown, so one bad marketplace
|
|
194
|
+
* never takes the browser down. */
|
|
195
|
+
warnings: string[];
|
|
196
|
+
}
|
|
197
|
+
export type ActionKind = "install" | "remove" | "enable" | "disable" | "update" | "market-add" | "market-update" | "market-remove";
|
|
198
|
+
export interface ActionRequest {
|
|
199
|
+
kind: ActionKind;
|
|
200
|
+
/** Required for every plugin-scoped action. */
|
|
201
|
+
entry?: PluginEntry;
|
|
202
|
+
/** Required for every marketplace-scoped action on an already-registered
|
|
203
|
+
* marketplace (`market-update`, `market-remove`). */
|
|
204
|
+
marketplace?: string;
|
|
205
|
+
/** Git URL of the marketplace repo. Required by `market-add` only. */
|
|
206
|
+
url?: string;
|
|
207
|
+
/** Git ref the added marketplace tracks. `market-add` only; defaults to main. */
|
|
208
|
+
ref?: string;
|
|
209
|
+
/** The scope this action operates in — where `install` and `market-add` put
|
|
210
|
+
* their result, and which of two same-named marketplaces `market-remove`
|
|
211
|
+
* destroys. Ignored by actions that do not resolve a scope. */
|
|
212
|
+
scope?: WritableScope;
|
|
213
|
+
}
|
|
214
|
+
export interface ActionOutcome {
|
|
215
|
+
ok: boolean;
|
|
216
|
+
/** One line for the status row. */
|
|
217
|
+
message: string;
|
|
218
|
+
/** Extra lines for the detail pane — mounted commands, issues, git output. */
|
|
219
|
+
detail?: string[];
|
|
220
|
+
}
|
|
221
|
+
/** How an action presents before it runs: the progress line, and the question
|
|
222
|
+
* to put in a confirm modal when the action destroys something. */
|
|
223
|
+
export interface ActionPresentation {
|
|
224
|
+
/** Imperative label for the footer/menu, e.g. "Install into user scope". */
|
|
225
|
+
label: string;
|
|
226
|
+
/** Present-tense progress line, e.g. "Installing capture…". */
|
|
227
|
+
progress: string;
|
|
228
|
+
/** Non-empty only for destructive actions; the shell must confirm first. */
|
|
229
|
+
confirm?: string;
|
|
230
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The read model behind `crtr pkg browse` — one shape the catalog produces, the
|
|
2
|
+
// action layer consumes, and the panels render.
|
|
3
|
+
//
|
|
4
|
+
// The browser answers one question the flat `pkg market browse` listing cannot:
|
|
5
|
+
// *what does installing this actually do to my crtr?* So an entry carries its
|
|
6
|
+
// marketplace metadata (cheap, from the manifest index) separately from its
|
|
7
|
+
// capability surface (commands mounted, memory docs provided, transport), which
|
|
8
|
+
// costs a directory read and is resolved on demand for the focused row only.
|
|
9
|
+
export const INSTALLED_SOURCE_ID = "installed";
|
|
10
|
+
export function marketplaceSourceId(name, scope) {
|
|
11
|
+
return `market:${scope}:${name}`;
|
|
12
|
+
}
|
|
13
|
+
/** `crtr search web` — how a user types the command a node names. The one place
|
|
14
|
+
* a command path becomes a label, so no corner of the page invents its own. */
|
|
15
|
+
export function commandLabel(node) {
|
|
16
|
+
return `crtr ${node.path.join(" ")}`;
|
|
17
|
+
}
|
|
18
|
+
/** Every node in a command forest, parents before their children. */
|
|
19
|
+
export function flattenCommands(nodes) {
|
|
20
|
+
return nodes.flatMap((node) => [node, ...flattenCommands(node.children)]);
|
|
21
|
+
}
|
|
22
|
+
/** The version to show beside a plugin's name, anywhere in the page.
|
|
23
|
+
*
|
|
24
|
+
* One rule, one place: the version beside a name is always the one in play on
|
|
25
|
+
* THIS machine — what is installed, or what installing would get you. The
|
|
26
|
+
* marketplace's offer for an already-installed plugin is a different fact, and
|
|
27
|
+
* it appears only as the right-hand side of the update arrow in the status
|
|
28
|
+
* line. Headline and list row both call this so they cannot disagree. */
|
|
29
|
+
export function displayVersion(entry) {
|
|
30
|
+
return entry.install.kind === "installed" ? entry.install.version ?? entry.version : entry.version;
|
|
31
|
+
}
|
|
32
|
+
/** `<count> <noun>`, with the noun agreeing with the count. Every count this
|
|
33
|
+
* page renders goes through here so no corner of it reads "1 docs". */
|
|
34
|
+
export function plural(count, one, many) {
|
|
35
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
import { type BindingId, type BindingResolution } from "../../../core/keybindings/index.js";
|
|
3
|
+
/** Full-screen plugin browser. The data and mutation boundaries stay in the
|
|
4
|
+
* catalog/actions collaborators, leaving this class responsible for movement,
|
|
5
|
+
* disclosure, and the rendered page grammar. */
|
|
6
|
+
export declare class PkgBrowseShell implements Component, Focusable {
|
|
7
|
+
private readonly tui;
|
|
8
|
+
private readonly terminalRows;
|
|
9
|
+
private readonly bindings;
|
|
10
|
+
private catalog;
|
|
11
|
+
private sourceIndex;
|
|
12
|
+
private zone;
|
|
13
|
+
private readonly list;
|
|
14
|
+
private readonly detail;
|
|
15
|
+
private readonly capabilities;
|
|
16
|
+
private modal;
|
|
17
|
+
private busy;
|
|
18
|
+
private notice;
|
|
19
|
+
private _focused;
|
|
20
|
+
onCancel?: () => void;
|
|
21
|
+
constructor(tui: TUI, terminalRows: () => number, bindings: BindingResolution<BindingId>);
|
|
22
|
+
/** The page follows the user's own `crtr.setup.*` navigation gestures, the
|
|
23
|
+
* same ones `crtr sys settings` resolves, alongside its literal defaults —
|
|
24
|
+
* a remap must not leave one full-screen page moving and the other stuck. */
|
|
25
|
+
private matches;
|
|
26
|
+
/** The footer names keys, so it names the gestures actually bound. */
|
|
27
|
+
private label;
|
|
28
|
+
private isDown;
|
|
29
|
+
private isUp;
|
|
30
|
+
/** Move one zone to the right, or open the focused row. */
|
|
31
|
+
private isDescend;
|
|
32
|
+
/** Move one zone to the left. Escape is deliberately separate: in the dossier
|
|
33
|
+
* it pops the whole view stack where `h` folds one branch. */
|
|
34
|
+
private isAscend;
|
|
35
|
+
/** Tab cycles rail sources from anywhere. The resolved `tab-next` gesture
|
|
36
|
+
* also carries the horizontal arrows, which mean "into the panel" here, so
|
|
37
|
+
* those are excluded exactly as the settings shell excludes them. */
|
|
38
|
+
private cyclesSource;
|
|
39
|
+
set focused(value: boolean);
|
|
40
|
+
get focused(): boolean;
|
|
41
|
+
/** Required by pi-tui's `Component`, and deliberately a no-op: this page
|
|
42
|
+
* holds no render cache to drop. Every panel is recomputed each frame
|
|
43
|
+
* because capability reads and action results land between input events. */
|
|
44
|
+
invalidate(): void;
|
|
45
|
+
dispose(): void;
|
|
46
|
+
private sources;
|
|
47
|
+
private get source();
|
|
48
|
+
private syncSource;
|
|
49
|
+
private syncDetail;
|
|
50
|
+
private reload;
|
|
51
|
+
private goSource;
|
|
52
|
+
private moveList;
|
|
53
|
+
private clearActionNotice;
|
|
54
|
+
/** A live filter, an open modal, or a running action holds every key — the
|
|
55
|
+
* one predicate both `handleInput` and the footer consult, so the footer can
|
|
56
|
+
* never advertise a key the filter is swallowing. */
|
|
57
|
+
private panelOwnsKeyboard;
|
|
58
|
+
handleInput(data: string): void;
|
|
59
|
+
private enterRail;
|
|
60
|
+
private enterList;
|
|
61
|
+
private enterDetail;
|
|
62
|
+
private jumpToInitial;
|
|
63
|
+
/** The focused rail row as an action subject. Carries the marketplace's scope
|
|
64
|
+
* as well as its name: two marketplaces can share a name across the user and
|
|
65
|
+
* project scopes, and without the scope `availableActions` cannot tell which
|
|
66
|
+
* one a remove would destroy, so it offers nothing at all. */
|
|
67
|
+
private marketplaceSubject;
|
|
68
|
+
private triggerAction;
|
|
69
|
+
private openAdd;
|
|
70
|
+
private handleModal;
|
|
71
|
+
private submitAdd;
|
|
72
|
+
/** Where `a` registers a marketplace. Named once so the modal can state
|
|
73
|
+
* exactly what the action will do. */
|
|
74
|
+
private addScope;
|
|
75
|
+
private execute;
|
|
76
|
+
private beginBusy;
|
|
77
|
+
private finishAction;
|
|
78
|
+
/** The source rail. It scrolls like the list does: marketplace count is user
|
|
79
|
+
* controlled, so a fixed slice would let the selection sit below the last
|
|
80
|
+
* drawn row, moving invisibly while the badge the user is aiming for never
|
|
81
|
+
* appears. Group headings scroll with their group. */
|
|
82
|
+
private renderRail;
|
|
83
|
+
private renderPanel;
|
|
84
|
+
private renderModal;
|
|
85
|
+
private wrapModal;
|
|
86
|
+
private footer;
|
|
87
|
+
/** The footer names KEYS, not actions. An absent plugin offers a user-scope
|
|
88
|
+
* and a project-scope install — two actions behind one `i`, which the scope
|
|
89
|
+
* modal then chooses between — so the hints are deduped before joining. */
|
|
90
|
+
private availableActionHints;
|
|
91
|
+
render(width: number): string[];
|
|
92
|
+
}
|
|
93
|
+
export declare function runPkgBrowse(): Promise<void>;
|