@crouton-kit/crouter 0.3.78 → 0.3.79
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.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1228 -12
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -787
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +8 -3
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +9 -1
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +849 -803
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +37 -0
- package/dist/commands/sys/setup-core.js +138 -1
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +915 -171
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +71 -41
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +2 -5
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +2 -2
- package/dist/core/keybindings/catalog.d.ts +3 -3
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker.js +7 -6
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/recap.d.ts +1 -1
- package/dist/core/runtime/recap.js +50 -25
- package/dist/core/runtime/session-list-cache.d.ts +10 -0
- package/dist/core/runtime/session-list-cache.js +94 -26
- package/dist/core/runtime/spawn.js +1 -13
- package/dist/core/runtime/tmux.js +2 -1
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/canvas-recap.js +43 -17
- package/dist/types.d.ts +6 -13
- package/dist/types.js +0 -3
- package/package.json +7 -3
|
@@ -1,7 +1,164 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdtempSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
4
6
|
import { resolveBundledPiCliPath } from '../../../core/runtime/pi-cli.js';
|
|
7
|
+
import { BINDING_CATALOG, resolveKeybindings } from '../../../core/keybindings/index.js';
|
|
8
|
+
import { SetupKeybindingsDraft, buildSetupManifest } from '../setup-core.js';
|
|
9
|
+
import { SetupWizard } from '../setup.js';
|
|
10
|
+
import { applyTheme } from '../../../clients/attach/config-load.js';
|
|
11
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
12
|
+
// The provider panel only touches the TUI on async OAuth transitions (never in
|
|
13
|
+
// these keybindings-tab render/input tests) and reads auth from `agentDir`; a
|
|
14
|
+
// no-op TUI stub and an empty temp agent dir keep the wizard headless.
|
|
15
|
+
const stubTui = { requestRender() { } };
|
|
16
|
+
const agentDir = mkdtempSync(join(tmpdir(), 'setup-wizard-test-'));
|
|
17
|
+
test('setup keybinding draft edits, disables, resets, and discards transactionally', () => {
|
|
18
|
+
const draft = new SetupKeybindingsDraft({ 'crtr.attach.graph.toggle': ['alt+x'] });
|
|
19
|
+
assert.equal(draft.expression('crtr.attach.graph.toggle'), 'alt+x');
|
|
20
|
+
assert.equal(draft.dirty, false);
|
|
21
|
+
assert.equal(draft.edit('crtr.attach.graph.toggle', 'M-Y, ctrl+g').ok, true);
|
|
22
|
+
assert.deepEqual(draft.overrides()['crtr.attach.graph.toggle'], ['alt+shift+y', 'ctrl+g']);
|
|
23
|
+
assert.equal(draft.dirty, true);
|
|
24
|
+
assert.equal(draft.unbind('crtr.attach.graph.toggle').ok, true);
|
|
25
|
+
assert.deepEqual(draft.overrides()['crtr.attach.graph.toggle'], []);
|
|
26
|
+
draft.resetRow('crtr.attach.graph.toggle');
|
|
27
|
+
assert.equal(Object.hasOwn(draft.overrides(), 'crtr.attach.graph.toggle'), false);
|
|
28
|
+
draft.edit('crtr.attach.graph.toggle', 'alt+y');
|
|
29
|
+
draft.discard();
|
|
30
|
+
assert.deepEqual(draft.overrides(), { 'crtr.attach.graph.toggle': ['alt+x'] });
|
|
31
|
+
assert.equal(draft.dirty, false);
|
|
32
|
+
});
|
|
33
|
+
test('setup keybinding draft drops a member when the committed value equals the catalog default (N2 sparseness)', () => {
|
|
34
|
+
const draft = new SetupKeybindingsDraft(undefined);
|
|
35
|
+
const definition = BINDING_CATALOG.find((entry) => entry.id === 'crtr.attach.graph.toggle');
|
|
36
|
+
const defaultExpression = draft.expression(definition.id);
|
|
37
|
+
// Committing the seeded default verbatim must NOT record an override: the row
|
|
38
|
+
// stays sparse (no member) and the draft stays clean — no spurious unsaved/dot.
|
|
39
|
+
const result = draft.edit(definition.id, defaultExpression);
|
|
40
|
+
assert.equal(result.ok, true);
|
|
41
|
+
assert.equal(Object.hasOwn(draft.overrides(), definition.id), false);
|
|
42
|
+
assert.equal(draft.dirty, false);
|
|
43
|
+
// Editing to a real value records the member; re-committing the default drops it.
|
|
44
|
+
assert.equal(draft.edit(definition.id, 'ctrl+g').ok, true);
|
|
45
|
+
assert.deepEqual(draft.overrides()[definition.id], ['ctrl+g']);
|
|
46
|
+
assert.equal(draft.dirty, true);
|
|
47
|
+
assert.equal(draft.edit(definition.id, defaultExpression).ok, true);
|
|
48
|
+
assert.equal(Object.hasOwn(draft.overrides(), definition.id), false);
|
|
49
|
+
assert.equal(draft.dirty, false);
|
|
50
|
+
});
|
|
51
|
+
test('setup keybinding draft keeps the last valid cell value on syntax, collision, and safety errors', () => {
|
|
52
|
+
const draft = new SetupKeybindingsDraft(undefined);
|
|
53
|
+
const syntax = draft.edit('crtr.attach.graph.toggle', 'ctrl+not-a-key');
|
|
54
|
+
assert.equal(syntax.ok, false);
|
|
55
|
+
assert.deepEqual(draft.overrides(), {});
|
|
56
|
+
const collision = draft.edit('crtr.attach.graph.toggle', 'alt+m');
|
|
57
|
+
assert.equal(collision.ok, false);
|
|
58
|
+
assert.match(collision.diagnostics[0].message, /crtr\.mode\.cycle/);
|
|
59
|
+
assert.deepEqual(draft.overrides(), {});
|
|
60
|
+
const unsafe = draft.unbind('crtr.setup.keybindings.save');
|
|
61
|
+
assert.equal(unsafe.ok, false);
|
|
62
|
+
assert.match(unsafe.diagnostics[0].message, /save-keybindings/);
|
|
63
|
+
assert.deepEqual(draft.overrides(), {});
|
|
64
|
+
});
|
|
65
|
+
test('setup reset-all removes invalid and unknown raw members without cataloging pi bindings', () => {
|
|
66
|
+
const invalidRoot = new SetupKeybindingsDraft('alt+x');
|
|
67
|
+
assert.equal(invalidRoot.resolution().diagnostics[0]?.code, 'invalid-keybindings');
|
|
68
|
+
invalidRoot.resetAll();
|
|
69
|
+
assert.equal(invalidRoot.dirty, true);
|
|
70
|
+
assert.equal(invalidRoot.resolution().diagnostics.length, 0);
|
|
71
|
+
const draft = new SetupKeybindingsDraft({
|
|
72
|
+
'crtr.unknown': ['x'],
|
|
73
|
+
'crtr.attach.graph.toggle': 'alt+x',
|
|
74
|
+
});
|
|
75
|
+
assert.equal(draft.resolution().diagnostics.length, 2);
|
|
76
|
+
draft.resetAll();
|
|
77
|
+
assert.deepEqual(draft.overrides(), {});
|
|
78
|
+
assert.equal(draft.resolution().diagnostics.length, 0);
|
|
79
|
+
assert.equal(BINDING_CATALOG.some((entry) => entry.id.includes('app.') || entry.id.includes('tui.')), false);
|
|
80
|
+
});
|
|
81
|
+
test('setup keybinding draft refuses edits while the raw root is invalid until reset-all', () => {
|
|
82
|
+
const draft = new SetupKeybindingsDraft('alt+x');
|
|
83
|
+
assert.equal(draft.hasInvalidRoot(), true);
|
|
84
|
+
// An edit against an invalid root must NOT commit (the prior bug: it returned
|
|
85
|
+
// ok:true against an empty `current` while the row kept showing the default).
|
|
86
|
+
const rejected = draft.edit('crtr.attach.graph.toggle', 'ctrl+g');
|
|
87
|
+
assert.equal(rejected.ok, false);
|
|
88
|
+
assert.equal(rejected.diagnostics[0].code, 'invalid-keybindings');
|
|
89
|
+
assert.deepEqual(draft.overrides(), {});
|
|
90
|
+
// reset-all clears the invalid root; edits then succeed and display.
|
|
91
|
+
draft.resetAll();
|
|
92
|
+
assert.equal(draft.hasInvalidRoot(), false);
|
|
93
|
+
const accepted = draft.edit('crtr.attach.graph.toggle', 'ctrl+g');
|
|
94
|
+
assert.equal(accepted.ok, true);
|
|
95
|
+
assert.deepEqual(draft.overrides()['crtr.attach.graph.toggle'], ['ctrl+g']);
|
|
96
|
+
});
|
|
97
|
+
test('setup keybindings editor stays visible while paging moves the selection (m3)', () => {
|
|
98
|
+
applyTheme({ cwd: process.cwd() });
|
|
99
|
+
const openingBindings = resolveKeybindings(undefined);
|
|
100
|
+
// A short terminal forces a small scroll window so paging past the edit row
|
|
101
|
+
// is the reproducible m3 scenario (editor was blind-typing off-screen before).
|
|
102
|
+
const wizard = new SetupWizard(buildSetupManifest(), new Set(), [], stubTui, agentDir, openingBindings, undefined, () => 14);
|
|
103
|
+
wizard.focused = true;
|
|
104
|
+
const render = () => stripVTControlCharacters(wizard.render(100).join('\n'));
|
|
105
|
+
// Drive to the Keybindings tab (tab-next = Tab), open the editor on row 0 (Enter).
|
|
106
|
+
for (let i = 0; i < 5; i += 1)
|
|
107
|
+
wizard.handleInput('\t');
|
|
108
|
+
assert.match(render(), /Keybindings/);
|
|
109
|
+
wizard.handleInput('\r');
|
|
110
|
+
// Type a distinctive marker into the live editor buffer.
|
|
111
|
+
wizard.handleInput('zzz');
|
|
112
|
+
assert.match(render(), /zzz/, 'editor should echo typed input before paging');
|
|
113
|
+
// PageDown twice: selection moves off, but the editor row must remain on screen.
|
|
114
|
+
wizard.handleInput('\x1b[6~');
|
|
115
|
+
wizard.handleInput('\x1b[6~');
|
|
116
|
+
assert.match(render(), /zzz/, 'editor must stay visible (and keep capturing) after paging');
|
|
117
|
+
});
|
|
118
|
+
test('setup keybindings q-cancel with a dirty draft routes through discard-confirm, not a silent exit (N3)', () => {
|
|
119
|
+
applyTheme({ cwd: process.cwd() });
|
|
120
|
+
const wizard = new SetupWizard(buildSetupManifest(), new Set(), [], stubTui, agentDir, resolveKeybindings(undefined), undefined, () => 40);
|
|
121
|
+
wizard.focused = true;
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
wizard.onCancel = () => { cancelled = true; };
|
|
124
|
+
const render = () => stripVTControlCharacters(wizard.render(100).join('\n'));
|
|
125
|
+
for (let i = 0; i < 5; i += 1)
|
|
126
|
+
wizard.handleInput('\t');
|
|
127
|
+
// Make the draft dirty: unbind the selected row (delete key) so it has a real
|
|
128
|
+
// pending override.
|
|
129
|
+
wizard.handleInput('\x1b[3~');
|
|
130
|
+
assert.match(render(), /unsaved/, 'draft should be dirty after unbind');
|
|
131
|
+
// q (crtr.setup.cancel) in list mode must NOT drop the edits: it opens the same
|
|
132
|
+
// discard-confirm the Escape path uses.
|
|
133
|
+
wizard.handleInput('q');
|
|
134
|
+
assert.equal(cancelled, false, 'dirty draft must not silently exit on q');
|
|
135
|
+
assert.match(render(), /Discard every unsaved keybinding change\?/);
|
|
136
|
+
// A clean draft still exits on q (cancel elsewhere is unchanged).
|
|
137
|
+
const clean = new SetupWizard(buildSetupManifest(), new Set(), [], stubTui, agentDir, resolveKeybindings(undefined), undefined, () => 40);
|
|
138
|
+
let cleanCancelled = false;
|
|
139
|
+
clean.onCancel = () => { cleanCancelled = true; };
|
|
140
|
+
clean.focused = true;
|
|
141
|
+
for (let i = 0; i < 5; i += 1)
|
|
142
|
+
clean.handleInput('\t');
|
|
143
|
+
clean.handleInput('q');
|
|
144
|
+
assert.equal(cleanCancelled, true, 'clean draft still exits on q');
|
|
145
|
+
});
|
|
146
|
+
test('setup keybindings selection always renders across group boundaries in a short window (m4 residual)', () => {
|
|
147
|
+
applyTheme({ cwd: process.cwd() });
|
|
148
|
+
// Small viewport so a window spans several group headings + a diagnostic line —
|
|
149
|
+
// the fixed-reserve page math used to let the selected row fail to render here.
|
|
150
|
+
const wizard = new SetupWizard(buildSetupManifest(), new Set(), [], stubTui, agentDir, resolveKeybindings(undefined), undefined, () => 13);
|
|
151
|
+
wizard.focused = true;
|
|
152
|
+
const render = () => stripVTControlCharacters(wizard.render(100).join('\n'));
|
|
153
|
+
for (let i = 0; i < 5; i += 1)
|
|
154
|
+
wizard.handleInput('\t');
|
|
155
|
+
// Page/step down through the whole catalog; the selection marker must always render.
|
|
156
|
+
for (let step = 0; step < BINDING_CATALOG.length + 5; step += 1) {
|
|
157
|
+
assert.match(render(), /\u25b6/, `selection marker must render at step ${step}`);
|
|
158
|
+
wizard.handleInput('\x1b[6~'); // PageDown
|
|
159
|
+
wizard.handleInput('j'); // and a single-row step to hit every boundary alignment
|
|
160
|
+
}
|
|
161
|
+
});
|
|
5
162
|
test('resolveBundledPiCliPath uses crouter bundled pi instead of a standalone pi on PATH', () => {
|
|
6
163
|
const prevPath = process.env.PATH;
|
|
7
164
|
process.env.PATH = '';
|
|
@@ -8,6 +8,8 @@ import { lsRemote } from '../../core/git.js';
|
|
|
8
8
|
import { listNodes } from '../../core/canvas/index.js';
|
|
9
9
|
import { activeFaultForDisplay } from '../../core/canvas/render.js';
|
|
10
10
|
import { readFault } from '../../core/runtime/fault.js';
|
|
11
|
+
import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
12
|
+
import { SUBTREE_NAMES } from '../../build-root.js';
|
|
11
13
|
function pass(scope, name, message) {
|
|
12
14
|
return { scope, name, status: 'pass', message };
|
|
13
15
|
}
|
|
@@ -89,6 +91,41 @@ function faultRemediation(fault) {
|
|
|
89
91
|
logPath: fault.logPath,
|
|
90
92
|
};
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate the command manifest + executable path of every effective command
|
|
96
|
+
* plugin (the deduped, enabled set that actually composes into the tree).
|
|
97
|
+
* Purely static — the plugin binary is NEVER executed, and no remediation ever
|
|
98
|
+
* chmods or rewrites third-party plugin content (every fix is a human_action:
|
|
99
|
+
* disable/update/remove the plugin). Only plugins whose scope is in `scopes`
|
|
100
|
+
* are reported, so `--scope` narrows these checks too.
|
|
101
|
+
*/
|
|
102
|
+
function runCommandPluginChecks(scopes) {
|
|
103
|
+
const results = [];
|
|
104
|
+
const inScope = new Set(scopes);
|
|
105
|
+
const reserved = new Set(SUBTREE_NAMES);
|
|
106
|
+
// Validate the whole effective set together so cross-plugin top-level name
|
|
107
|
+
// collisions surface here — a collided command is NOT mounted, and every
|
|
108
|
+
// claimant must fail, not pass.
|
|
109
|
+
for (const v of validateEffectiveCommandPlugins(reserved)) {
|
|
110
|
+
const plugin = v.plugin;
|
|
111
|
+
if (!inScope.has(plugin.scope))
|
|
112
|
+
continue;
|
|
113
|
+
if (v.issues.length === 0) {
|
|
114
|
+
const mounts = v.contributions.map((c) => c.node.name).join(', ');
|
|
115
|
+
results.push(pass(plugin.scope, `plugin:${plugin.name}:commands`, mounts.length > 0 ? `contributes top-level command(s): ${mounts}` : `command manifest valid`));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
v.issues.forEach((issue, i) => {
|
|
119
|
+
const remediation = {
|
|
120
|
+
kind: 'human_action',
|
|
121
|
+
description: `${issue.next} If unfixable, disable (pkg plugin disable ${plugin.name}), update (pkg plugin update --name ${plugin.name}), or remove (pkg plugin remove ${plugin.name}) the plugin.`,
|
|
122
|
+
};
|
|
123
|
+
const detail = issue.path !== undefined ? ` at ${issue.path}` : '';
|
|
124
|
+
results.push(failCheck(plugin.scope, `plugin:${plugin.name}:commands[${i}]`, `${issue.code}${detail}: ${issue.message} (received ${issue.received}, expected ${issue.expected})`, remediation));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
92
129
|
function runFaultChecks() {
|
|
93
130
|
const results = [];
|
|
94
131
|
for (const node of listNodes()) {
|
|
@@ -240,7 +277,7 @@ function runChecksForScope(scope, opts) {
|
|
|
240
277
|
export const sysDoctorLeaf = defineLeaf({
|
|
241
278
|
name: 'doctor',
|
|
242
279
|
description: 'diagnose installation health',
|
|
243
|
-
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing,
|
|
280
|
+
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or a command-plugin manifest/executable is malformed. Reports each problem with a structured remediation, and can apply the safe repairs for you.',
|
|
244
281
|
help: {
|
|
245
282
|
name: 'sys doctor',
|
|
246
283
|
summary: 'diagnose missing manifests and broken config entries',
|
|
@@ -250,13 +287,13 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
250
287
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
251
288
|
],
|
|
252
289
|
output: [
|
|
253
|
-
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action. Sorted by scope then name.' },
|
|
290
|
+
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. scope may be user | project | canvas. Command-plugin manifest checks appear as plugin:<name>:commands (validated statically, never executed); their remediation is always a human_action (disable/update/remove the plugin). remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | human_action. Sorted by scope then name.' },
|
|
254
291
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
255
292
|
],
|
|
256
293
|
outputKind: 'object',
|
|
257
294
|
effects: [
|
|
258
|
-
'Read-only unless --fix is passed.',
|
|
259
|
-
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings stay read-only.',
|
|
295
|
+
'Read-only unless --fix is passed. Command-plugin manifests are validated statically — the plugin executable is never run, at any time.',
|
|
296
|
+
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories; canvas fault findings and command-plugin findings stay read-only (--fix never chmods or rewrites third-party plugin content).',
|
|
260
297
|
'Each non-pass result carries a structured `remediation` describing the fix action (absolute paths, exact config keys, or a human_action with logPath) so callers can apply it directly without --fix.',
|
|
261
298
|
],
|
|
262
299
|
},
|
|
@@ -270,6 +307,7 @@ export const sysDoctorLeaf = defineLeaf({
|
|
|
270
307
|
const results = runChecksForScope(scope, { fix, remote });
|
|
271
308
|
allResults.push(...results);
|
|
272
309
|
}
|
|
310
|
+
allResults.push(...runCommandPluginChecks(scopes));
|
|
273
311
|
allResults.push(...runFaultChecks());
|
|
274
312
|
// Sort by scope then name (mirrors old printResults grouping)
|
|
275
313
|
const scopeOrder = { user: 0, project: 1, builtin: 2, canvas: 3 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BindingId, type BindingResolution, type KeybindingDiagnostic, type SparseKeybindingOverrides } from '../../core/keybindings/index.js';
|
|
1
2
|
export type SetupPackageId = 'crtr-extensions' | 'mode-switch' | 'oauth-adapter';
|
|
2
3
|
export type SetupTool = 'tmux' | 'nvim';
|
|
3
4
|
export type SetupPackageSourceKind = 'local-path' | 'npm';
|
|
@@ -58,6 +59,33 @@ export interface SetupInitialState {
|
|
|
58
59
|
exaKeyPath: string;
|
|
59
60
|
composioKeyPath: string;
|
|
60
61
|
}
|
|
62
|
+
export interface KeybindingEditResult {
|
|
63
|
+
readonly ok: boolean;
|
|
64
|
+
readonly diagnostics: readonly KeybindingDiagnostic[];
|
|
65
|
+
}
|
|
66
|
+
/** Transactional sparse keybinding draft used by the full-screen setup tab. */
|
|
67
|
+
export declare class SetupKeybindingsDraft {
|
|
68
|
+
private expected;
|
|
69
|
+
private baseline;
|
|
70
|
+
private current;
|
|
71
|
+
private baselineInvalidRoot;
|
|
72
|
+
private invalidRoot;
|
|
73
|
+
constructor(rawKeybindings: unknown);
|
|
74
|
+
get dirty(): boolean;
|
|
75
|
+
overrides(): Record<string, unknown>;
|
|
76
|
+
expectedKeybindings(): unknown;
|
|
77
|
+
resolution(): BindingResolution<BindingId>;
|
|
78
|
+
/** True while the persisted `keybindings` root is a non-object (e.g. a string):
|
|
79
|
+
* edits cannot be layered onto an invalid root, so the tab must reset-all first. */
|
|
80
|
+
hasInvalidRoot(): boolean;
|
|
81
|
+
expression(id: BindingId): string;
|
|
82
|
+
edit(id: BindingId, expression: string): KeybindingEditResult;
|
|
83
|
+
unbind(id: BindingId): KeybindingEditResult;
|
|
84
|
+
resetRow(id: BindingId): void;
|
|
85
|
+
resetAll(): void;
|
|
86
|
+
discard(): void;
|
|
87
|
+
markSaved(overrides: SparseKeybindingOverrides<BindingId>): void;
|
|
88
|
+
}
|
|
61
89
|
export declare function buildSetupManifest(resolveBuiltinPackageDir?: (name: string) => string): SetupPackageManifestEntry[];
|
|
62
90
|
export declare function normalizePackageIdentity(source: string, settingsDir: string, homeDir: string): SetupPackageIdentity;
|
|
63
91
|
export declare function detectInstalledSetupPackageIds(settingsJson: unknown, manifest?: SetupPackageManifestEntry[], settingsPath?: string, homeDir?: string): Set<SetupPackageId>;
|
|
@@ -68,6 +96,15 @@ export declare function buildSystemToolState(tool: SetupTool, packageManager: Pa
|
|
|
68
96
|
export declare function resolveSettingsPath(homeDir?: string): string;
|
|
69
97
|
export declare function resolveExaKeyPath(homeDir?: string): string;
|
|
70
98
|
export declare function resolveComposioKeyPath(homeDir?: string): string;
|
|
99
|
+
/** Where an already-present API key is resolved from — env var wins over the
|
|
100
|
+
* key file, matching the runtime resolvers (search/exa.ts, connectors). */
|
|
101
|
+
export type ApiKeySource = 'env' | 'file';
|
|
102
|
+
export interface ApiKeyPresence {
|
|
103
|
+
present: boolean;
|
|
104
|
+
source?: ApiKeySource;
|
|
105
|
+
}
|
|
106
|
+
export declare function detectExaKey(homeDir?: string): ApiKeyPresence;
|
|
107
|
+
export declare function detectComposioKey(homeDir?: string): ApiKeyPresence;
|
|
71
108
|
export declare function buildSetupInitialState(opts?: {
|
|
72
109
|
manifest?: SetupPackageManifestEntry[];
|
|
73
110
|
settingsPath?: string;
|
|
@@ -1,10 +1,131 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { chmod, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import { BINDING_CATALOG, normalizeGesture, resolveKeybindings, } from '../../core/keybindings/index.js';
|
|
5
8
|
import { builtinPiPackageDir } from '../../core/scope.js';
|
|
6
9
|
import { readJsonIfExists } from '../../core/fs-utils.js';
|
|
7
10
|
import { resolveBundledPiCliPath } from '../../core/runtime/pi-cli.js';
|
|
11
|
+
function isKeybindingsObject(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function cloneKeybindings(value) {
|
|
15
|
+
if (!isKeybindingsObject(value))
|
|
16
|
+
return {};
|
|
17
|
+
return Object.fromEntries(Object.entries(value).map(([id, gestures]) => [
|
|
18
|
+
id,
|
|
19
|
+
Array.isArray(gestures) ? [...gestures] : gestures,
|
|
20
|
+
]));
|
|
21
|
+
}
|
|
22
|
+
/** Transactional sparse keybinding draft used by the full-screen setup tab. */
|
|
23
|
+
export class SetupKeybindingsDraft {
|
|
24
|
+
expected;
|
|
25
|
+
baseline;
|
|
26
|
+
current;
|
|
27
|
+
baselineInvalidRoot;
|
|
28
|
+
invalidRoot;
|
|
29
|
+
constructor(rawKeybindings) {
|
|
30
|
+
this.expected = rawKeybindings;
|
|
31
|
+
this.baseline = cloneKeybindings(rawKeybindings);
|
|
32
|
+
this.current = cloneKeybindings(rawKeybindings);
|
|
33
|
+
this.baselineInvalidRoot = rawKeybindings !== undefined && !isKeybindingsObject(rawKeybindings) ? rawKeybindings : undefined;
|
|
34
|
+
this.invalidRoot = this.baselineInvalidRoot;
|
|
35
|
+
}
|
|
36
|
+
get dirty() {
|
|
37
|
+
return !isDeepStrictEqual(this.current, this.baseline)
|
|
38
|
+
|| !isDeepStrictEqual(this.invalidRoot, this.baselineInvalidRoot);
|
|
39
|
+
}
|
|
40
|
+
overrides() {
|
|
41
|
+
return cloneKeybindings(this.current);
|
|
42
|
+
}
|
|
43
|
+
expectedKeybindings() {
|
|
44
|
+
return this.expected;
|
|
45
|
+
}
|
|
46
|
+
resolution() {
|
|
47
|
+
return resolveKeybindings(this.invalidRoot === undefined ? this.current : this.invalidRoot);
|
|
48
|
+
}
|
|
49
|
+
/** True while the persisted `keybindings` root is a non-object (e.g. a string):
|
|
50
|
+
* edits cannot be layered onto an invalid root, so the tab must reset-all first. */
|
|
51
|
+
hasInvalidRoot() {
|
|
52
|
+
return this.invalidRoot !== undefined;
|
|
53
|
+
}
|
|
54
|
+
expression(id) {
|
|
55
|
+
return this.resolution().gestures(id).join(', ');
|
|
56
|
+
}
|
|
57
|
+
edit(id, expression) {
|
|
58
|
+
if (this.invalidRoot !== undefined) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
diagnostics: [{
|
|
62
|
+
severity: 'error',
|
|
63
|
+
code: 'invalid-keybindings',
|
|
64
|
+
offendingValue: this.invalidRoot,
|
|
65
|
+
message: 'Reset all before editing: the saved keybindings root is invalid.',
|
|
66
|
+
}],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const definition = BINDING_CATALOG.find((candidate) => candidate.id === id);
|
|
70
|
+
let gestures;
|
|
71
|
+
try {
|
|
72
|
+
gestures = expression.trim() === ''
|
|
73
|
+
? []
|
|
74
|
+
: expression.split(',').map((gesture) => normalizeGesture(gesture.trim(), definition));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
diagnostics: [{
|
|
80
|
+
severity: 'error',
|
|
81
|
+
code: 'invalid-override',
|
|
82
|
+
bindingId: id,
|
|
83
|
+
offendingValue: expression,
|
|
84
|
+
message: error instanceof Error ? error.message : String(error),
|
|
85
|
+
}],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const deduped = [...new Set(gestures)];
|
|
89
|
+
// Sparse invariant: a member is stored only when it changes the effective
|
|
90
|
+
// resolution. If the committed gestures equal the catalog default for this
|
|
91
|
+
// id, drop the member instead of recording a redundant override — otherwise
|
|
92
|
+
// committing an unchanged default flips the row to a spurious user/unsaved
|
|
93
|
+
// state that persistence would then strip anyway.
|
|
94
|
+
const withoutId = { ...this.current };
|
|
95
|
+
delete withoutId[id];
|
|
96
|
+
const defaultGestures = [...resolveKeybindings(withoutId).gestures(id)];
|
|
97
|
+
const prospective = isDeepStrictEqual(deduped, defaultGestures)
|
|
98
|
+
? withoutId
|
|
99
|
+
: { ...this.current, [id]: deduped };
|
|
100
|
+
const resolution = resolveKeybindings(prospective);
|
|
101
|
+
const relevant = resolution.diagnostics.filter((diagnostic) => diagnostic.bindingId === id || diagnostic.relatedBindingIds?.includes(id));
|
|
102
|
+
if (relevant.length > 0)
|
|
103
|
+
return { ok: false, diagnostics: relevant };
|
|
104
|
+
this.current = prospective;
|
|
105
|
+
return { ok: true, diagnostics: resolution.diagnostics };
|
|
106
|
+
}
|
|
107
|
+
unbind(id) {
|
|
108
|
+
return this.edit(id, '');
|
|
109
|
+
}
|
|
110
|
+
resetRow(id) {
|
|
111
|
+
delete this.current[id];
|
|
112
|
+
}
|
|
113
|
+
resetAll() {
|
|
114
|
+
this.current = {};
|
|
115
|
+
this.invalidRoot = undefined;
|
|
116
|
+
}
|
|
117
|
+
discard() {
|
|
118
|
+
this.current = cloneKeybindings(this.baseline);
|
|
119
|
+
this.invalidRoot = this.baselineInvalidRoot;
|
|
120
|
+
}
|
|
121
|
+
markSaved(overrides) {
|
|
122
|
+
this.expected = Object.keys(overrides).length === 0 ? undefined : cloneKeybindings(overrides);
|
|
123
|
+
this.baseline = cloneKeybindings(overrides);
|
|
124
|
+
this.current = cloneKeybindings(overrides);
|
|
125
|
+
this.baselineInvalidRoot = undefined;
|
|
126
|
+
this.invalidRoot = undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
8
129
|
const PACKAGE_COPY = {
|
|
9
130
|
'crtr-extensions': {
|
|
10
131
|
name: 'pi-crtr-extensions',
|
|
@@ -234,6 +355,22 @@ export function resolveExaKeyPath(homeDir = homedir()) {
|
|
|
234
355
|
export function resolveComposioKeyPath(homeDir = homedir()) {
|
|
235
356
|
return join(homeDir, '.crouter', 'composio.key');
|
|
236
357
|
}
|
|
358
|
+
function detectApiKey(envVar, keyPath) {
|
|
359
|
+
if ((process.env[envVar] ?? '').trim() !== '')
|
|
360
|
+
return { present: true, source: 'env' };
|
|
361
|
+
try {
|
|
362
|
+
if (readFileSync(keyPath, 'utf8').trim() !== '')
|
|
363
|
+
return { present: true, source: 'file' };
|
|
364
|
+
}
|
|
365
|
+
catch { /* absent/unreadable → not present */ }
|
|
366
|
+
return { present: false };
|
|
367
|
+
}
|
|
368
|
+
export function detectExaKey(homeDir = homedir()) {
|
|
369
|
+
return detectApiKey('EXA_API_KEY', resolveExaKeyPath(homeDir));
|
|
370
|
+
}
|
|
371
|
+
export function detectComposioKey(homeDir = homedir()) {
|
|
372
|
+
return detectApiKey('COMPOSIO_API_KEY', resolveComposioKeyPath(homeDir));
|
|
373
|
+
}
|
|
237
374
|
export function buildSetupInitialState(opts = {}) {
|
|
238
375
|
const homeDir = opts.homeDir ?? homedir();
|
|
239
376
|
const settingsPath = opts.settingsPath ?? resolveSettingsPath(homeDir);
|
|
@@ -275,7 +412,7 @@ export function renderSetupStaticInstructions(state) {
|
|
|
275
412
|
return [
|
|
276
413
|
instructions.headline,
|
|
277
414
|
'',
|
|
278
|
-
'Provider login: open the wizard in a TTY and use the
|
|
415
|
+
'Provider login: open the wizard in a TTY and use the Providers tab (same flow as /login) to authenticate a model provider.',
|
|
279
416
|
'',
|
|
280
417
|
'Packages:',
|
|
281
418
|
...instructions.packageLines,
|
|
@@ -1,2 +1,90 @@
|
|
|
1
1
|
import type { LeafDef } from '../../core/command.js';
|
|
2
|
+
import { TUI, type Component, type Focusable } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type BindingId, type BindingResolution } from '../../core/keybindings/index.js';
|
|
4
|
+
import { type SetupPackageId, type SetupPackageManifestEntry, type SetupSubmission, type SetupSystemToolState } from './setup-core.js';
|
|
5
|
+
export declare class SetupWizard implements Component, Focusable {
|
|
6
|
+
private readonly manifest;
|
|
7
|
+
private readonly installedPackageIds;
|
|
8
|
+
private readonly systemTools;
|
|
9
|
+
private readonly exaInput;
|
|
10
|
+
private readonly composioInput;
|
|
11
|
+
private readonly keyInput;
|
|
12
|
+
private readonly openingBindings;
|
|
13
|
+
private readonly keyDraft;
|
|
14
|
+
private readonly terminalRows;
|
|
15
|
+
private readonly selectedPackageIds;
|
|
16
|
+
private readonly selectedSystemTools;
|
|
17
|
+
private readonly providers;
|
|
18
|
+
private activeTab;
|
|
19
|
+
private pluginIndex;
|
|
20
|
+
private systemIndex;
|
|
21
|
+
private expandedPluginId;
|
|
22
|
+
private confirming;
|
|
23
|
+
private keyIndex;
|
|
24
|
+
private keyScroll;
|
|
25
|
+
private keyFilter;
|
|
26
|
+
private filteringKeys;
|
|
27
|
+
private editingKey;
|
|
28
|
+
private keyEditError;
|
|
29
|
+
private keyDiagnostics;
|
|
30
|
+
private keyConfirm;
|
|
31
|
+
private keyNotice;
|
|
32
|
+
private cachedWidth;
|
|
33
|
+
private cachedLines;
|
|
34
|
+
private _focused;
|
|
35
|
+
onSubmit?: (submission: SetupSubmission) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
constructor(manifest: SetupPackageManifestEntry[], installedPackageIds: Set<SetupPackageId>, systemTools: SetupSystemToolState[], tui: TUI, agentDir: string, openingBindings: BindingResolution<BindingId>, rawKeybindings: unknown, terminalRows: () => number);
|
|
38
|
+
set focused(value: boolean);
|
|
39
|
+
get focused(): boolean;
|
|
40
|
+
invalidate(): void;
|
|
41
|
+
private setTab;
|
|
42
|
+
private moveTab;
|
|
43
|
+
private cancel;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
private matches;
|
|
46
|
+
/** Human hint label for an action, derived from the opening snapshot so footers
|
|
47
|
+
* reflect the effective (possibly remapped) gestures the wizard actually honors. */
|
|
48
|
+
private label;
|
|
49
|
+
/** Like `label`, but drops gestures a focused pi text Input consumes as data
|
|
50
|
+
* (single printable chars and the arrow keys), so the Exa/Composio footers do
|
|
51
|
+
* not claim keys that never reach the wizard while a field owns focus. */
|
|
52
|
+
private fieldSafeLabel;
|
|
53
|
+
private handleGlobalTabSwitch;
|
|
54
|
+
private selectedPackage;
|
|
55
|
+
private selectedSystem;
|
|
56
|
+
private movePlugin;
|
|
57
|
+
private moveSystem;
|
|
58
|
+
private togglePackage;
|
|
59
|
+
private toggleSystem;
|
|
60
|
+
private toggleDetails;
|
|
61
|
+
private hasPendingInstalls;
|
|
62
|
+
private buildSubmission;
|
|
63
|
+
private finalize;
|
|
64
|
+
private filteredKeybindings;
|
|
65
|
+
private selectedKeybinding;
|
|
66
|
+
private moveKey;
|
|
67
|
+
private keyViewportLines;
|
|
68
|
+
private keyPageSize;
|
|
69
|
+
private beginKeyEdit;
|
|
70
|
+
private commitKeyEdit;
|
|
71
|
+
private cancelKeyEdit;
|
|
72
|
+
private saveKeybindings;
|
|
73
|
+
private refreshTmuxBindingsAfterSave;
|
|
74
|
+
private confirmKeyAction;
|
|
75
|
+
private handleKeybindingsInput;
|
|
76
|
+
handleInput(data: string): void;
|
|
77
|
+
private renderHeader;
|
|
78
|
+
private renderFooter;
|
|
79
|
+
private renderProviders;
|
|
80
|
+
private renderPlugins;
|
|
81
|
+
private renderSystems;
|
|
82
|
+
private renderExa;
|
|
83
|
+
private renderComposio;
|
|
84
|
+
private renderConfirm;
|
|
85
|
+
private keyRowHasMessage;
|
|
86
|
+
private keyLineCost;
|
|
87
|
+
private renderKeybindings;
|
|
88
|
+
render(width: number): string[];
|
|
89
|
+
}
|
|
2
90
|
export declare const sysSetupLeaf: LeafDef;
|