@north-light/crouter 0.3.184 → 0.3.186
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/api/dto/broker.d.ts +0 -32
- package/dist/api/dto/messages.d.ts +0 -5
- 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/input/controller.js +3 -4
- package/dist/clients/attach/overlays/mcp.js +1 -3
- package/dist/clients/attach/slash/dispatch.js +12 -2
- package/dist/clients/attach/viewer.js +349 -349
- package/dist/commands/memory/list.js +7 -3
- 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/__tests__/session-model.test.js +2 -4
- 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/broker/event-projection.d.ts +1 -8
- package/dist/core/runtime/broker/event-projection.js +1 -25
- package/dist/core/runtime/broker/frame-dispatch.d.ts +0 -2
- package/dist/core/runtime/broker/frame-dispatch.js +8 -41
- package/dist/core/runtime/broker-protocol.d.ts +1 -46
- package/dist/core/runtime/broker.js +1 -8
- package/dist/core/runtime/interactive-deliver.d.ts +2 -5
- package/dist/core/runtime/interactive-deliver.js +3 -6
- package/dist/core/runtime/tmux-bindings.js +2 -0
- package/dist/core/session-model/session-state.d.ts +3 -6
- package/dist/core/session-model/session-state.js +1 -5
- package/dist/core/tui/page-host.d.ts +25 -0
- package/dist/core/tui/page-host.js +65 -0
- package/dist/daemon/api/handlers/messages.js +1 -7
- 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/core/runtime/broker/message-ledger.d.ts +0 -53
- package/dist/core/runtime/broker/message-ledger.js +0 -143
- /package/dist/{commands/sys/panels → core/tui}/panel.d.ts +0 -0
- /package/dist/{commands/sys/panels → core/tui}/panel.js +0 -0
|
@@ -155,6 +155,7 @@ export const marketUpdate = defineLeaf({
|
|
|
155
155
|
summary: 'git pull one or all registered marketplaces',
|
|
156
156
|
params: [
|
|
157
157
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Marketplace name to update. Omit to update all registered marketplaces.' },
|
|
158
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
158
159
|
],
|
|
159
160
|
output: [
|
|
160
161
|
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per marketplace processed: {name, updated, sha}.' },
|
|
@@ -164,6 +165,8 @@ export const marketUpdate = defineLeaf({
|
|
|
164
165
|
},
|
|
165
166
|
run: async (input) => {
|
|
166
167
|
const name = input['name'];
|
|
168
|
+
const scopeInput = input['scope'];
|
|
169
|
+
const scope = scopeInput === undefined ? undefined : resolveScopeArg(scopeInput);
|
|
167
170
|
function pullIfGit(root) {
|
|
168
171
|
const shaBefore = currentSha(root);
|
|
169
172
|
if (!isGitRepo(root))
|
|
@@ -189,7 +192,7 @@ export const marketUpdate = defineLeaf({
|
|
|
189
192
|
}
|
|
190
193
|
s.marketplaces[target.name].last_updated = nowIso();
|
|
191
194
|
});
|
|
192
|
-
const sourcedPlugins = listAllPlugins().filter((plugin) => plugin.scope
|
|
195
|
+
const sourcedPlugins = listAllPlugins().filter((plugin) => plugin.scope === target.scope && plugin.sourceMarketplace === target.name);
|
|
193
196
|
for (const plugin of sourcedPlugins) {
|
|
194
197
|
if (isSymlink(plugin.root)) {
|
|
195
198
|
// Marketplace pull above updates symlinked relative-source plugins.
|
|
@@ -217,13 +220,19 @@ export const marketUpdate = defineLeaf({
|
|
|
217
220
|
return results;
|
|
218
221
|
}
|
|
219
222
|
if (name !== undefined) {
|
|
220
|
-
const found =
|
|
223
|
+
const found = scope === undefined || scope === 'all' || scope === 'builtin'
|
|
224
|
+
? findMarketplaceByName(name)
|
|
225
|
+
: findMarketplaceByName(name, scope);
|
|
221
226
|
if (!found)
|
|
222
|
-
throw notFound(`marketplace not found: ${name}`);
|
|
227
|
+
throw notFound(`marketplace not found: ${name}${scope === undefined || scope === 'all' || scope === 'builtin' ? '' : ` (scope: ${scope})`}`);
|
|
223
228
|
const results = await doUpdate([{ name: found.name, scope: found.scope, root: found.root }]);
|
|
224
229
|
return { updated: results };
|
|
225
230
|
}
|
|
226
|
-
const targets =
|
|
231
|
+
const targets = (scope === undefined || scope === 'all'
|
|
232
|
+
? listAllMarketplaces()
|
|
233
|
+
: scope === 'builtin'
|
|
234
|
+
? []
|
|
235
|
+
: listAllMarketplaces().filter((marketplace) => marketplace.scope === scope)).map((m) => ({ name: m.name, scope: m.scope, root: m.root }));
|
|
227
236
|
const results = await doUpdate(targets);
|
|
228
237
|
return { updated: results };
|
|
229
238
|
},
|
|
@@ -549,6 +549,7 @@ export const pluginUpdate = defineLeaf({
|
|
|
549
549
|
summary: 'refresh one or all installed plugins',
|
|
550
550
|
params: [
|
|
551
551
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Plugin name to update. Omit to update all installed plugins.' },
|
|
552
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
552
553
|
],
|
|
553
554
|
output: [
|
|
554
555
|
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, commands?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier and docs is the written memory-document count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}.' },
|
|
@@ -558,15 +559,19 @@ export const pluginUpdate = defineLeaf({
|
|
|
558
559
|
},
|
|
559
560
|
run: async (input) => {
|
|
560
561
|
const name = input['name'];
|
|
562
|
+
const scopeInput = input['scope'];
|
|
563
|
+
const scope = scopeInput === undefined ? undefined : resolveScopeArg(scopeInput);
|
|
561
564
|
const marketplaceCache = new Map();
|
|
562
565
|
if (name !== undefined) {
|
|
563
|
-
const found =
|
|
566
|
+
const found = scope === undefined || scope === 'all' || scope === 'builtin'
|
|
567
|
+
? findPluginByName(name)
|
|
568
|
+
: findPluginByName(name, scope);
|
|
564
569
|
if (found === null) {
|
|
565
|
-
throw notFound(`plugin not found: ${name}`);
|
|
570
|
+
throw notFound(`plugin not found: ${name}${scope === undefined || scope === 'all' || scope === 'builtin' ? '' : ` (scope: ${scope})`}`);
|
|
566
571
|
}
|
|
567
572
|
return { updated: [await updateOnePlugin(found, marketplaceCache, { strict: true })] };
|
|
568
573
|
}
|
|
569
|
-
const targets = listAllPlugins().filter((
|
|
574
|
+
const targets = listAllPlugins().filter((plugin) => plugin.scope !== 'builtin' && (scope === undefined || scope === 'all' || plugin.scope === scope));
|
|
570
575
|
const results = [];
|
|
571
576
|
for (const plugin of targets) {
|
|
572
577
|
try {
|
package/dist/commands/pkg.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `crtr pkg` subtree: plugin lifecycle and marketplace source management.
|
|
2
2
|
import { defineBranch } from '../core/command.js';
|
|
3
|
+
import { pkgBrowseLeaf } from './pkg/browse.js';
|
|
3
4
|
import { pluginBranch } from './pkg/plugin.js';
|
|
4
5
|
import { marketBranch } from './pkg/market.js';
|
|
5
6
|
export function registerPkg() {
|
|
@@ -14,6 +15,6 @@ export function registerPkg() {
|
|
|
14
15
|
name: 'pkg',
|
|
15
16
|
summary: 'manage plugins and marketplace sources',
|
|
16
17
|
},
|
|
17
|
-
children: [pluginBranch, marketBranch],
|
|
18
|
+
children: [pkgBrowseLeaf, pluginBranch, marketBranch],
|
|
18
19
|
});
|
|
19
20
|
}
|
|
@@ -114,18 +114,20 @@ async function runCommandPluginChecks(scopes) {
|
|
|
114
114
|
const inScope = new Set(scopes);
|
|
115
115
|
const reserved = new Set(SUBTREE_NAMES);
|
|
116
116
|
// Validate the whole effective set together so cross-plugin top-level name
|
|
117
|
-
// collisions surface here
|
|
118
|
-
// claimant
|
|
117
|
+
// collisions surface here. A collision no longer costs anyone their command:
|
|
118
|
+
// each claimant mounts under an origin-qualified name, so it is reported as a
|
|
119
|
+
// healthy mount rather than a failure demanding the user remove a plugin.
|
|
119
120
|
for (const v of validateEffectiveCommandPlugins(reserved, undefined, undefined, await coreCommandPaths())) {
|
|
120
121
|
const plugin = v.plugin;
|
|
121
122
|
if (!inScope.has(plugin.scope))
|
|
122
123
|
continue;
|
|
123
|
-
|
|
124
|
+
const blocking = v.issues.filter((issue) => issue.code !== 'command_collision');
|
|
125
|
+
if (blocking.length === 0) {
|
|
124
126
|
const mounts = v.contributions.map((c) => c.node.name).join(', ');
|
|
125
127
|
results.push(pass(plugin.scope, `plugin:${plugin.name}:commands`, mounts.length > 0 ? `contributes top-level command(s): ${mounts}` : `command manifest valid`));
|
|
126
128
|
continue;
|
|
127
129
|
}
|
|
128
|
-
|
|
130
|
+
blocking.forEach((issue, i) => {
|
|
129
131
|
const remediation = {
|
|
130
132
|
kind: 'human_action',
|
|
131
133
|
description: `${issue.next} Inspect with \`pkg plugin show ${plugin.name}\`; then update (\`pkg plugin update --name ${plugin.name}\`) or remove (\`pkg plugin remove ${plugin.name}\`) the plugin.`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Input } from "@earendil-works/pi-tui";
|
|
2
|
-
import { type SetupPanel } from "
|
|
2
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
3
3
|
export declare class BrokerLimitsPanel implements SetupPanel {
|
|
4
4
|
private readonly onChange;
|
|
5
5
|
readonly warningInput: Input;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Input, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { readConfig, updateRawConfigAtomically } from "../../../core/config.js";
|
|
3
3
|
import { BROKER_LIMIT_OVERRIDE_PRECEDENCE } from "../setup.js";
|
|
4
|
-
import { padAnsi, theme, wrapText } from "
|
|
4
|
+
import { padAnsi, theme, wrapText } from "../../../core/tui/panel.js";
|
|
5
5
|
function positive(value) {
|
|
6
6
|
return /^\d+$/.test(value) &&
|
|
7
7
|
Number.isSafeInteger(Number(value)) &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SetupSystemInstallPlan } from "../setup-core.js";
|
|
2
|
-
import { type SetupPanel } from "
|
|
2
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
3
3
|
export declare class DependenciesPanel implements SetupPanel {
|
|
4
4
|
private readonly plans;
|
|
5
5
|
private readonly onChange;
|
|
@@ -2,7 +2,7 @@ import { Input, matchesKey, truncateToWidth, visibleWidth, } from "@earendil-wor
|
|
|
2
2
|
import { BINDING_CATALOG, formatBinding, formatGesture, isAttachPaneBinding, matchesPiTuiInput, persistUserKeybindings, KeybindingConcurrentEditError, KeybindingValidationError, } from "../../../core/keybindings/index.js";
|
|
3
3
|
import { installTmuxBindings, occupiedTmuxRootGestures, } from "../../../core/runtime/tmux-chrome.js";
|
|
4
4
|
import { SetupKeybindingsDraft } from "../setup-core.js";
|
|
5
|
-
import { padAnsi, theme } from "
|
|
5
|
+
import { padAnsi, theme } from "../../../core/tui/panel.js";
|
|
6
6
|
/** Collapse explicitly-paired catalog definitions into one setup action row.
|
|
7
7
|
* Pair membership and field roles come only from catalog metadata; neither
|
|
8
8
|
* rendering nor installation guesses from binding ID names. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { BindingId } from "../../../core/keybindings/index.js";
|
|
4
|
-
import { type SetupPanel } from "
|
|
4
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
5
5
|
export declare class ModelsPanel implements SetupPanel {
|
|
6
6
|
private readonly tui;
|
|
7
7
|
private readonly runtime;
|
|
@@ -6,7 +6,7 @@ import { readRawConfigAtRoot, persistDefaultKindModel, persistModelLadders, read
|
|
|
6
6
|
import { findProjectScopeRoots, userScopeRoot } from "../../../core/scope.js";
|
|
7
7
|
import { loadProfileManifest, profileRoot, } from "../../../core/profiles/manifest.js";
|
|
8
8
|
import { configuredLadderProviders, inferPortableModelSelection, MODEL_PROVIDER_KEYS, MODEL_STRENGTHS, THINKING_SUFFIXES, } from "../../../core/runtime/model-selection.js";
|
|
9
|
-
import { fitTail, padAnsi, renderRows, theme, } from "
|
|
9
|
+
import { fitTail, padAnsi, renderRows, theme, } from "../../../core/tui/panel.js";
|
|
10
10
|
const MODELS_CHROME_LINES = 3;
|
|
11
11
|
const RUNTIME_PROVIDER_ID = {
|
|
12
12
|
anthropic: ANTHROPIC_PROVIDER_ID,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SetupPackageId, SetupPackageManifestEntry } from "../setup-core.js";
|
|
2
|
-
import { type SetupPanel } from "
|
|
2
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
3
3
|
export declare class PluginsPanel implements SetupPanel {
|
|
4
4
|
private readonly manifest;
|
|
5
5
|
private readonly installedPackageIds;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type UserSettingGroup } from "../../../core/user-settings.js";
|
|
2
|
-
import { type SetupPanel } from "
|
|
2
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
3
3
|
/**
|
|
4
4
|
* One destination's worth of user-scope preference rows. The old single
|
|
5
5
|
* "Settings" tab listed every row flat with a permanent description under each,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { readConfig, updateConfig } from "../../../core/config.js";
|
|
3
3
|
import { userSettingsInGroup, } from "../../../core/user-settings.js";
|
|
4
|
-
import { padAnsi, theme, visibleRange, wrapText, } from "
|
|
4
|
+
import { padAnsi, theme, visibleRange, wrapText, } from "../../../core/tui/panel.js";
|
|
5
5
|
const LABEL_WIDTH = 26;
|
|
6
6
|
/**
|
|
7
7
|
* One destination's worth of user-scope preference rows. The old single
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import { type SetupPanel } from "
|
|
3
|
+
import { type SetupPanel } from "../../../core/tui/panel.js";
|
|
4
4
|
type AuthStatus = ReturnType<ModelRegistry["getProviderAuthStatus"]>;
|
|
5
5
|
type ProviderAuthType = "oauth" | "api_key";
|
|
6
6
|
type ProviderRow = {
|
|
@@ -5,7 +5,7 @@ import { dialogAuthInteraction } from "../../../core/auth-interaction.js";
|
|
|
5
5
|
import { isManagedProvider, promoteSubscription, readSubscriptionPool, renameSubscription, } from "../../../core/subscription-state.js";
|
|
6
6
|
import { removeProviderRow } from "../../../core/provider-management.js";
|
|
7
7
|
import { cliClient } from "../../api-client.js";
|
|
8
|
-
import { fitTail, theme, visibleRange, } from "
|
|
8
|
+
import { fitTail, theme, visibleRange, } from "../../../core/tui/panel.js";
|
|
9
9
|
/** Curated setup-catalog view over pi's OAuth providers + the model registry. */
|
|
10
10
|
export function buildProviderCatalog(runtime, registry) {
|
|
11
11
|
const oauth = runtime
|
|
@@ -10,7 +10,7 @@ import { runSetupSettingsProcess } from "./setup-settings-runner.js";
|
|
|
10
10
|
import { BrokerLimitsPanel } from "./panels/broker-limits-panel.js";
|
|
11
11
|
import { KeybindingsPanel } from "./panels/keybindings-panel.js";
|
|
12
12
|
import { ModelsPanel } from "./panels/models-panel.js";
|
|
13
|
-
import { PAGE_CHROME_ROWS, columns, padAnsi, pageFrame, theme, } from "
|
|
13
|
+
import { PAGE_CHROME_ROWS, columns, padAnsi, pageFrame, theme, } from "../../core/tui/panel.js";
|
|
14
14
|
import { PrefsPanel } from "./panels/prefs-panel.js";
|
|
15
15
|
import { ProviderPanel } from "./panels/provider-panel.js";
|
|
16
16
|
/** Rail column width, including its badge gutter. */
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type SetupSettingsProcessContext = {
|
|
9
|
-
tui: TUI;
|
|
10
|
-
terminal: ProcessTerminal;
|
|
2
|
+
import type { BindingId, BindingResolution } from "../../core/keybindings/index.js";
|
|
3
|
+
import { type TuiPageContext, type TuiPageControl, type TuiPageSurface } from "../../core/tui/page-host.js";
|
|
4
|
+
/** Setup and settings both edit pi's model configuration, so their host adds
|
|
5
|
+
* the model runtime to the shared page context. Every other page concern —
|
|
6
|
+
* theme, keybindings, terminal lifecycle — belongs to `runTuiPage`. */
|
|
7
|
+
type SetupSettingsProcessContext = TuiPageContext & {
|
|
11
8
|
agentDir: string;
|
|
12
9
|
runtime: ModelRuntime;
|
|
13
10
|
registry: ModelRegistry;
|
|
14
|
-
bindings: BindingResolution<BindingId>;
|
|
15
|
-
rawUserConfig: ReturnType<typeof readRawScopeConfig>;
|
|
16
|
-
};
|
|
17
|
-
type SetupSettingsProcessControl<Result> = {
|
|
18
|
-
submit(result: Result): void;
|
|
19
|
-
cancel(): void;
|
|
20
11
|
};
|
|
21
|
-
|
|
22
|
-
* submission, and panel policy stay in their concrete surface factories. */
|
|
23
|
-
export declare function runSetupSettingsProcess<Result>(createSurface: (context: SetupSettingsProcessContext, control: SetupSettingsProcessControl<Result>) => SetupSettingsSurface, interceptCancel?: (data: string, bindings: BindingResolution<BindingId>) => boolean): Promise<Result | undefined>;
|
|
12
|
+
export declare function runSetupSettingsProcess<Result>(createSurface: (context: SetupSettingsProcessContext, control: TuiPageControl<Result>) => TuiPageSurface, interceptCancel?: (data: string, bindings: BindingResolution<BindingId>) => boolean): Promise<Result | undefined>;
|
|
24
13
|
export {};
|
|
@@ -1,63 +1,14 @@
|
|
|
1
1
|
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { ProcessTerminal, TUI, } from "@earendil-works/pi-tui";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { readRawScopeConfig } from "../../core/config.js";
|
|
6
|
-
import { resolveKeybindings, } from "../../core/keybindings/index.js";
|
|
3
|
+
import { defaultAgentDir } from "../../clients/attach/config.js";
|
|
7
4
|
import { PoolCredentialStore } from "../../core/pool-credential-store.js";
|
|
8
|
-
|
|
9
|
-
* submission, and panel policy stay in their concrete surface factories. */
|
|
5
|
+
import { runTuiPage, } from "../../core/tui/page-host.js";
|
|
10
6
|
export async function runSetupSettingsProcess(createSurface, interceptCancel) {
|
|
11
|
-
applyTheme({ cwd: process.cwd() });
|
|
12
7
|
const agentDir = defaultAgentDir();
|
|
13
8
|
const runtime = await ModelRuntime.create({
|
|
14
9
|
credentials: new PoolCredentialStore(join(agentDir, "auth.json")),
|
|
15
10
|
modelsPath: join(agentDir, "models.json"),
|
|
16
11
|
});
|
|
17
12
|
const registry = new ModelRegistry(runtime);
|
|
18
|
-
|
|
19
|
-
const bindings = resolveKeybindings(rawUserConfig?.keybindings);
|
|
20
|
-
const terminal = new ProcessTerminal();
|
|
21
|
-
const tui = new TUI(terminal, true);
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
let surface;
|
|
24
|
-
let restored = false;
|
|
25
|
-
const restore = () => {
|
|
26
|
-
if (restored)
|
|
27
|
-
return;
|
|
28
|
-
restored = true;
|
|
29
|
-
surface?.dispose();
|
|
30
|
-
try {
|
|
31
|
-
tui.stop();
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
/* terminal restore */
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const finish = (result) => {
|
|
38
|
-
restore();
|
|
39
|
-
resolve(result);
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
surface = createSurface({ tui, terminal, agentDir, runtime, registry, bindings, rawUserConfig }, { submit: finish, cancel: () => finish(undefined) });
|
|
43
|
-
tui.addChild(surface);
|
|
44
|
-
tui.setFocus(surface);
|
|
45
|
-
process.once("exit", restore);
|
|
46
|
-
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"])
|
|
47
|
-
process.once(signal, () => finish(undefined));
|
|
48
|
-
if (interceptCancel !== undefined) {
|
|
49
|
-
tui.addInputListener((data) => {
|
|
50
|
-
if (!interceptCancel(data, bindings))
|
|
51
|
-
return undefined;
|
|
52
|
-
finish(undefined);
|
|
53
|
-
return { consume: true };
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
tui.start();
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
restore();
|
|
60
|
-
reject(error);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
13
|
+
return runTuiPage((context, control) => createSurface({ ...context, agentDir, runtime, registry }, control), interceptCancel);
|
|
63
14
|
}
|
|
@@ -10,7 +10,7 @@ import { formatGesture, matchesPiTuiInput, } from "../../core/keybindings/index.
|
|
|
10
10
|
import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, packagePlanAction, reconcileInboxFrontDoor, renderSetupStaticInstructions, } from "./setup-core.js";
|
|
11
11
|
import { runSetupSettingsProcess } from "./setup-settings-runner.js";
|
|
12
12
|
import { DependenciesPanel } from "./panels/dependencies-panel.js";
|
|
13
|
-
import { PAGE_CHROME_ROWS, pageFrame, theme, } from "
|
|
13
|
+
import { PAGE_CHROME_ROWS, pageFrame, theme, } from "../../core/tui/panel.js";
|
|
14
14
|
import { PluginsPanel } from "./panels/plugins-panel.js";
|
|
15
15
|
import { ProviderPanel } from "./panels/provider-panel.js";
|
|
16
16
|
const STEPS = ["Providers", "Plugins", "Dependencies"];
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// real BranchDef/LeafDef whose root/branch/leaf help carries the plugin's
|
|
11
11
|
// representations, schema, effects, and stable order.
|
|
12
12
|
// 2. Core collision — a fixture claiming a SUBTREE_NAMES name is skipped +
|
|
13
|
-
// recorded as a command_collision issue
|
|
13
|
+
// recorded as a command_collision issue; plugin collisions keep both
|
|
14
|
+
// commands under origin-qualified names.
|
|
14
15
|
// 3. Scope/shadowing — a project copy shadows a user copy of the same plugin
|
|
15
16
|
// name; a disabled project copy suppresses the user copy.
|
|
16
17
|
// 4. Protocol integration (table-driven, one fixture executable) — request
|
|
@@ -39,10 +40,6 @@ import { CrtrError } from '../../errors.js';
|
|
|
39
40
|
import { resetScopeCache } from '../../scope.js';
|
|
40
41
|
import { listInstalledPluginsInRoot } from '../../resolver.js';
|
|
41
42
|
import { pluginShow } from '../../../commands/pkg/plugin-inspect.js';
|
|
42
|
-
import { sysDoctorLeaf } from '../../../commands/sys/doctor.js';
|
|
43
|
-
import { createApiServer } from '../../../daemon/api/server.js';
|
|
44
|
-
import { apiSocketPath, crtrHome } from '../../canvas/paths.js';
|
|
45
|
-
import { closeDb } from '../../canvas/db.js';
|
|
46
43
|
// The set of reserved core names discovery must fail closed against. We don't
|
|
47
44
|
// import the real SUBTREE_NAMES (build-root.ts pulls the whole tree); a small
|
|
48
45
|
// explicit set is enough for the collision contract and keeps the test cheap.
|
|
@@ -156,8 +153,12 @@ function installPlugin(scopeRoot, name, opts = {}) {
|
|
|
156
153
|
const execPath = join(root, 'bin', 'cmd.js');
|
|
157
154
|
writeFileSync(execPath, opts.exec ?? FIXTURE_EXEC);
|
|
158
155
|
chmodSync(execPath, opts.execBit === false ? 0o644 : 0o755);
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
const config = {
|
|
157
|
+
...(opts.enabled === false ? { enabled: false } : {}),
|
|
158
|
+
...(opts.sourceMarketplace !== undefined ? { source_marketplace: opts.sourceMarketplace } : {}),
|
|
159
|
+
};
|
|
160
|
+
if (Object.keys(config).length > 0) {
|
|
161
|
+
writeFileSync(join(scopeRoot, 'config.json'), JSON.stringify({ plugins: { [name]: config } }));
|
|
161
162
|
}
|
|
162
163
|
return root;
|
|
163
164
|
}
|
|
@@ -599,8 +600,8 @@ describe('plugin help addenda', () => {
|
|
|
599
600
|
});
|
|
600
601
|
// ---------------------------------------------------------------------------
|
|
601
602
|
// Lifecycle surfaces — the validator's verdict must reach the caller WITHOUT
|
|
602
|
-
// executing the plugin binary, and a cross-plugin top-level collision must
|
|
603
|
-
//
|
|
603
|
+
// executing the plugin binary, and a cross-plugin top-level collision must
|
|
604
|
+
// preserve every claimant under its origin-qualified command name.
|
|
604
605
|
// ---------------------------------------------------------------------------
|
|
605
606
|
describe('lifecycle surfaces report validation without executing the plugin', () => {
|
|
606
607
|
// Records the fact it ran. No validation surface may ever spawn it.
|
|
@@ -639,45 +640,25 @@ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true, result: { ap
|
|
|
639
640
|
assert.equal(commands.mounts.length, 0, 'a rejected manifest contributes no mounts');
|
|
640
641
|
assertNotExecuted();
|
|
641
642
|
});
|
|
642
|
-
test('a cross-plugin top-level collision
|
|
643
|
-
// Two plugins, each valid in isolation, both claiming `app`.
|
|
643
|
+
test('a cross-plugin top-level collision keeps both commands under their origin-qualified names', async () => {
|
|
644
|
+
// Two plugins, each valid in isolation, both claiming `app`. One is direct;
|
|
645
|
+
// the other records its marketplace as the command origin.
|
|
644
646
|
installPlugin(userRoot, 'p-one', { exec: SENTINEL_EXEC });
|
|
645
|
-
installPlugin(userRoot, 'p-two', { exec: SENTINEL_EXEC });
|
|
647
|
+
installPlugin(userRoot, 'p-two', { exec: SENTINEL_EXEC, sourceMarketplace: 'fixture-market' });
|
|
646
648
|
resetScopeCache();
|
|
647
649
|
const snapshot = discoverCommandContributions(new Set(['sys']), emptyStart);
|
|
648
|
-
assert.
|
|
649
|
-
assert.deepEqual(snapshot.issues.filter((i) => i.code === 'command_collision').map((i) => i.plugin).sort(), ['p-one', 'p-two'], 'both claimants
|
|
650
|
-
for (const name of ['p-one', 'p-two']) {
|
|
650
|
+
assert.deepEqual(snapshot.contributions.map((contribution) => contribution.node.name).sort(), ['fixture-market:app', 'p-one:app']);
|
|
651
|
+
assert.deepEqual(snapshot.issues.filter((i) => i.code === 'command_collision').map((i) => i.plugin).sort(), ['p-one', 'p-two'], 'both claimants report their resolved mount');
|
|
652
|
+
for (const [name, mount] of [['p-one', 'p-one:app'], ['p-two', 'fixture-market:app']]) {
|
|
651
653
|
const out = await pluginShow.run({ name });
|
|
652
654
|
const commands = out['commands'];
|
|
653
|
-
assert.deepEqual(commands.mounts, [], `${name}
|
|
655
|
+
assert.deepEqual(commands.mounts, [mount], `${name} lists its qualified command as an accepted mount`);
|
|
654
656
|
const issue = commands.issues.find((i) => i.code === 'command_collision');
|
|
655
657
|
assert.ok(issue, `${name} show must carry the command_collision issue`);
|
|
656
658
|
assert.deepEqual(issue.received.split(', ').sort(), ['p-one', 'p-two'], 'the issue names both claimants');
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// here) rather than trying to autostart a subprocess.
|
|
661
|
-
mkdirSync(crtrHome(), { recursive: true });
|
|
662
|
-
const server = createApiServer();
|
|
663
|
-
try {
|
|
664
|
-
for (let i = 0; i < 200 && !existsSync(apiSocketPath()); i++)
|
|
665
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
666
|
-
assert.equal(existsSync(apiSocketPath()), true, 'api socket never bound');
|
|
667
|
-
const out = await sysDoctorLeaf.run({ scope: 'user', fix: false, remote: false });
|
|
668
|
-
const checks = out['checks'];
|
|
669
|
-
for (const name of ['p-one', 'p-two']) {
|
|
670
|
-
assert.equal(checks.find((k) => k.name === `plugin:${name}:commands` && k.status === 'pass'), undefined, `${name} must not PASS while its command is dropped by a collision`);
|
|
671
|
-
const failCheck = checks.find((k) => k.name.startsWith(`plugin:${name}:commands`) && k.status === 'fail' && /command_collision/.test(k.message));
|
|
672
|
-
assert.ok(failCheck, `${name} must fail with command_collision, got ${JSON.stringify(checks.map((k) => k.name + ':' + k.status))}`);
|
|
673
|
-
assert.match(failCheck.message, /claimed by multiple plugins/);
|
|
674
|
-
assert.equal(failCheck.remediation?.kind, 'human_action', 'command remediation is never auto-applied');
|
|
675
|
-
}
|
|
676
|
-
assert.equal(out['ok'], false);
|
|
677
|
-
}
|
|
678
|
-
finally {
|
|
679
|
-
await server.close();
|
|
680
|
-
closeDb();
|
|
659
|
+
assert.match(issue.message, new RegExp(`mounted as "${mount}"`));
|
|
660
|
+
assert.equal(issue.expected, 'an origin-qualified top-level command for each claimant');
|
|
661
|
+
assert.equal(issue.next, `Invoke "${mount}" to use this plugin command.`);
|
|
681
662
|
}
|
|
682
663
|
assertNotExecuted();
|
|
683
664
|
});
|
|
@@ -98,12 +98,10 @@ test('a welcome is a COMPLETE catch-up: a re-welcome drops a dead broker s level
|
|
|
98
98
|
const s0 = fold(initialSessionState(), [
|
|
99
99
|
welcome(),
|
|
100
100
|
{ type: 'extension_ui_request', id: 'u1', method: 'setStatus', statusKey: 'gone', statusText: 'stale' },
|
|
101
|
-
{ type: 'queue_update', steering: ['one', 'two'], followUp: []
|
|
101
|
+
{ type: 'queue_update', steering: ['one', 'two'], followUp: [] },
|
|
102
102
|
]);
|
|
103
103
|
assert.equal(s0.display.statuses['gone'], 'stale');
|
|
104
|
-
|
|
105
|
-
// short array) still renders as a bare-text entry.
|
|
106
|
-
assert.deepEqual(s0.queued, [{ id: 'id-1', text: 'one' }, { text: 'two' }]);
|
|
104
|
+
assert.deepEqual(s0.queued, [{ text: 'one' }, { text: 'two' }]);
|
|
107
105
|
assert.equal(s0.engine?.pendingMessageCount, 2);
|
|
108
106
|
// A fresh broker (post-revive) welcomes with its OWN levels and an empty queue,
|
|
109
107
|
// and emits no queue_update to overwrite the panel — so the welcome must.
|
|
@@ -4,7 +4,13 @@ import type { DeclLeafBase, DeclBranch, CommandManifestIssue } from './schema.js
|
|
|
4
4
|
export type CommandContributorRef = {
|
|
5
5
|
name: string;
|
|
6
6
|
scope: 'user' | 'project';
|
|
7
|
+
sourceMarketplace?: string;
|
|
7
8
|
};
|
|
9
|
+
/** The stable source name used to qualify a colliding plugin command. */
|
|
10
|
+
export declare function commandContributorOrigin(contributor: Pick<CommandContributorRef, 'name' | 'sourceMarketplace'>): string;
|
|
11
|
+
/** The top-level name assigned to a plugin command that collides with another
|
|
12
|
+
* plugin command. */
|
|
13
|
+
export declare function qualifiedTopLevelCommandName(contributor: Pick<CommandContributorRef, 'name' | 'sourceMarketplace'>, commandName: string): string;
|
|
8
14
|
/** Adapter callback to turn a declared leaf into a crtr LeafDef. Takes the
|
|
9
15
|
* leaf definition and its resolved command path, returns the outputKind and
|
|
10
16
|
* run function to be used by the tree composer. */
|
|
@@ -34,7 +40,8 @@ export interface CommandRegistrySnapshot {
|
|
|
34
40
|
issues: ContributorIssue[];
|
|
35
41
|
}
|
|
36
42
|
/** Resolve a flat list of candidate contributions into a collision-resolved
|
|
37
|
-
* snapshot.
|
|
38
|
-
*
|
|
39
|
-
* results are deterministically ordered by contributor
|
|
43
|
+
* snapshot. Plugin claimants at a collided path are qualified by origin and
|
|
44
|
+
* kept; core names take precedence and are dropped. An issue is recorded on
|
|
45
|
+
* every claimant, and results are deterministically ordered by contributor
|
|
46
|
+
* name. */
|
|
40
47
|
export declare function resolveCommandRegistry(candidates: readonly CommandContribution[], reservedCoreNames: ReadonlySet<string>): CommandRegistrySnapshot;
|