@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
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// Command-plugin discovery + validation.
|
|
2
|
+
//
|
|
3
|
+
// An installed, enabled plugin may declare `commands?: string` in its
|
|
4
|
+
// `plugin.json` — a plugin-root-relative path to one declarative command
|
|
5
|
+
// manifest (`commands.json`). This module reads and validates those manifests
|
|
6
|
+
// WITHOUT executing any plugin code, turning each into either an accepted
|
|
7
|
+
// contribution (a validated top-level branch node) or one or more typed
|
|
8
|
+
// discovery issues. crtr owns parse/help/render/errors; the plugin owns one
|
|
9
|
+
// executable that the adapter direct-spawns per leaf invocation.
|
|
10
|
+
//
|
|
11
|
+
// Everything here is invocation-local and side-effect-free: no cache, no
|
|
12
|
+
// persisted state, no spawning. `discoverCommandContributions` is called only
|
|
13
|
+
// on the fallthrough (non-core-fast-path) command path (see `build-root.ts`);
|
|
14
|
+
// `validateEffectiveCommandPlugins` is the entry point Phase 2 lifecycle
|
|
15
|
+
// surfaces (`pkg plugin show`, `sys doctor`) consume to report issues — it
|
|
16
|
+
// carries cross-plugin collision issues that the per-plugin
|
|
17
|
+
// `validatePluginCommands` structurally cannot see.
|
|
18
|
+
import { realpathSync, readFileSync, statSync } from 'node:fs';
|
|
19
|
+
import { resolve, sep } from 'node:path';
|
|
20
|
+
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
|
|
21
|
+
import { projectScopeRoots } from '../scope.js';
|
|
22
|
+
const TIERS = new Set(['normal', 'common', 'important']);
|
|
23
|
+
const FLAG_TYPES = new Set(['string', 'int', 'bool', 'path', 'enum']);
|
|
24
|
+
const KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
25
|
+
const MANIFEST_KEYS = new Set(['schemaVersion', 'executable', 'mounts']);
|
|
26
|
+
const MOUNT_KEYS = new Set(['parent', 'node']);
|
|
27
|
+
const BRANCH_KEYS = new Set([
|
|
28
|
+
'kind', 'name', 'description', 'whenToUse', 'tier', 'rootEntry', 'summary', 'model', 'children',
|
|
29
|
+
]);
|
|
30
|
+
const LEAF_KEYS = new Set([
|
|
31
|
+
'kind', 'name', 'description', 'whenToUse', 'tier', 'summary', 'params', 'output', 'outputKind', 'effects',
|
|
32
|
+
]);
|
|
33
|
+
const ROOT_ENTRY_KEYS = new Set(['concept', 'description', 'whenToUse']);
|
|
34
|
+
// Allowed keys are per-kind: a key valid on a flag (e.g. `default`, `choices`)
|
|
35
|
+
// is rejected on a stdin param, and `repeatable`/`hidden` are not external API
|
|
36
|
+
// at all (repeatable is unimplemented; external plugins cannot declare hidden
|
|
37
|
+
// commands) — so they are unknown keys everywhere. This keeps the strict
|
|
38
|
+
// reject-unknown posture from silently dropping a declared-but-ignored key.
|
|
39
|
+
const PARAM_KEYS_BY_KIND = {
|
|
40
|
+
positional: new Set(['kind', 'name', 'type', 'required', 'constraint']),
|
|
41
|
+
flag: new Set(['kind', 'name', 'type', 'choices', 'required', 'constraint', 'default']),
|
|
42
|
+
stdin: new Set(['kind', 'name', 'required', 'constraint']),
|
|
43
|
+
'context-file': new Set(['kind', 'name', 'required', 'constraint', 'shape']),
|
|
44
|
+
};
|
|
45
|
+
const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint']);
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Effective plugin resolution (M3 — reuse, no new resolver)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/** The effective, deduped set of enabled plugins that declare a command
|
|
50
|
+
* manifest, in scope-precedence order. Enumerated with existing machinery
|
|
51
|
+
* only: `projectScopeRoots()` (profile-widened, nearest-first) via
|
|
52
|
+
* `listInstalledPluginsInRoot('project', root)`, then user scope. Dedup is by
|
|
53
|
+
* plugin NAME across ALL copies (highest precedence wins) BEFORE the enabled
|
|
54
|
+
* filter — so a disabled higher-precedence copy suppresses lower-precedence
|
|
55
|
+
* copies of the same name. */
|
|
56
|
+
export function effectiveCommandPlugins(startDir = process.cwd(), profileId) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const deduped = [];
|
|
59
|
+
const consider = (p) => {
|
|
60
|
+
if (seen.has(p.name))
|
|
61
|
+
return;
|
|
62
|
+
seen.add(p.name);
|
|
63
|
+
deduped.push(p);
|
|
64
|
+
};
|
|
65
|
+
for (const root of projectScopeRoots(startDir, profileId)) {
|
|
66
|
+
for (const p of listInstalledPluginsInRoot('project', root))
|
|
67
|
+
consider(p);
|
|
68
|
+
}
|
|
69
|
+
for (const p of listInstalledPlugins('user'))
|
|
70
|
+
consider(p);
|
|
71
|
+
return deduped.filter((p) => p.enabled && typeof p.manifest.commands === 'string' && p.manifest.commands.length > 0);
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Discovery snapshot (tree composer entry point)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/** Validate every effective command plugin and compose an invocation-local
|
|
77
|
+
* registry snapshot. `reservedNames` are the core top-level subtree names —
|
|
78
|
+
* a contribution claiming one is silently skipped from the tree and recorded
|
|
79
|
+
* as a `command_collision` issue (core always wins). A plugin-vs-plugin
|
|
80
|
+
* top-level name collision drops all claimants with an issue. Accepted
|
|
81
|
+
* contributions are sorted by name for deterministic help/order. */
|
|
82
|
+
export function discoverCommandContributions(reservedNames, startDir = process.cwd(), profileId) {
|
|
83
|
+
const validations = validateEffectiveCommandPlugins(reservedNames, startDir, profileId);
|
|
84
|
+
const contributions = validations.flatMap((v) => v.contributions);
|
|
85
|
+
contributions.sort((a, b) => a.node.name.localeCompare(b.node.name));
|
|
86
|
+
return { contributions, issues: validations.flatMap((v) => v.issues) };
|
|
87
|
+
}
|
|
88
|
+
/** Validate every effective command plugin, including the cross-plugin
|
|
89
|
+
* top-level name collision pass: a name claimed by more than one plugin is
|
|
90
|
+
* dropped from EVERY claimant's contributions and a `command_collision`
|
|
91
|
+
* issue is recorded on EACH claimant's validation, so per-plugin surfaces
|
|
92
|
+
* (`sys doctor`, `pkg plugin show`) see it too. This is the entry point for
|
|
93
|
+
* any surface that reports on mounted commands — the per-plugin
|
|
94
|
+
* `validatePluginCommands` cannot see other plugins' claims. */
|
|
95
|
+
export function validateEffectiveCommandPlugins(reservedNames, startDir = process.cwd(), profileId) {
|
|
96
|
+
const validations = effectiveCommandPlugins(startDir, profileId).map((p) => validatePluginCommands(p, reservedNames));
|
|
97
|
+
// Plugin-vs-plugin top-level name collision: drop all claimants + flag each.
|
|
98
|
+
const claimants = new Map();
|
|
99
|
+
for (const v of validations) {
|
|
100
|
+
for (const c of v.contributions) {
|
|
101
|
+
const list = claimants.get(c.node.name);
|
|
102
|
+
if (list !== undefined)
|
|
103
|
+
list.push(v);
|
|
104
|
+
else
|
|
105
|
+
claimants.set(c.node.name, [v]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const [name, vs] of claimants) {
|
|
109
|
+
if (vs.length === 1)
|
|
110
|
+
continue;
|
|
111
|
+
const names = vs.map((x) => x.plugin.name).join(', ');
|
|
112
|
+
for (const v of vs) {
|
|
113
|
+
v.contributions = v.contributions.filter((c) => c.node.name !== name);
|
|
114
|
+
v.issues.push({
|
|
115
|
+
code: 'command_collision',
|
|
116
|
+
plugin: v.plugin.name,
|
|
117
|
+
path: name,
|
|
118
|
+
message: `top-level command "${name}" is claimed by multiple plugins: ${names}`,
|
|
119
|
+
received: names,
|
|
120
|
+
expected: 'exactly one plugin per top-level command name',
|
|
121
|
+
next: `Disable or remove all but one of: ${names}.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return validations;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Per-plugin validation (Phase 2 lifecycle entry point)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/** Validate one plugin's command manifest without executing its binary.
|
|
131
|
+
* `reservedNames` are the core top-level names used for the core-collision
|
|
132
|
+
* check; pass an empty set to skip it. Never throws. */
|
|
133
|
+
export function validatePluginCommands(plugin, reservedNames = new Set()) {
|
|
134
|
+
const issues = [];
|
|
135
|
+
const contributions = [];
|
|
136
|
+
const rel = plugin.manifest.commands ?? '';
|
|
137
|
+
const manifestPath = resolve(plugin.root, rel);
|
|
138
|
+
const issue = (code, message, received, expected, next, path) => {
|
|
139
|
+
issues.push({ code, plugin: plugin.name, ...(path !== undefined ? { path } : {}), message, received, expected, next });
|
|
140
|
+
};
|
|
141
|
+
// Load + parse the manifest under path-safety confinement.
|
|
142
|
+
const safe = safePath(plugin.root, rel);
|
|
143
|
+
if (safe === null) {
|
|
144
|
+
issue('command_path_unsafe', `commands pointer escapes the plugin root or is not a regular file`, rel, 'a relative path to a regular file inside the plugin root', 'Fix the `commands` field in plugin.json.');
|
|
145
|
+
return { plugin, manifestPath, contributions, issues };
|
|
146
|
+
}
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = readFileSync(safe, 'utf8');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
issue('command_manifest_unreadable', `commands.json is missing or unreadable`, safe, 'a readable JSON file', 'Reinstall or update the plugin.');
|
|
153
|
+
return { plugin, manifestPath, contributions, issues };
|
|
154
|
+
}
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
issue('command_manifest_invalid', `commands.json is not valid JSON`, safe, 'a JSON object', 'Regenerate commands.json from the plugin\'s command definitions.');
|
|
161
|
+
return { plugin, manifestPath, contributions, issues };
|
|
162
|
+
}
|
|
163
|
+
if (!isObject(parsed)) {
|
|
164
|
+
issue('command_manifest_invalid', `commands.json must be a JSON object`, typeName(parsed), 'a JSON object', 'Regenerate commands.json.');
|
|
165
|
+
return { plugin, manifestPath, contributions, issues };
|
|
166
|
+
}
|
|
167
|
+
// Top-level shape: exact schemaVersion, no unknown keys.
|
|
168
|
+
const unknown = Object.keys(parsed).filter((k) => !MANIFEST_KEYS.has(k));
|
|
169
|
+
if (unknown.length > 0) {
|
|
170
|
+
issue('command_manifest_invalid', `commands.json has unknown top-level keys`, unknown.join(', '), 'only: schemaVersion, executable, mounts', 'Remove the unknown keys.');
|
|
171
|
+
return { plugin, manifestPath, contributions, issues };
|
|
172
|
+
}
|
|
173
|
+
if (parsed['schemaVersion'] !== 1) {
|
|
174
|
+
issue('command_schema_version', `unsupported command schemaVersion`, String(parsed['schemaVersion']), 'exactly the integer 1', 'Update the plugin to a version compatible with this crtr.');
|
|
175
|
+
return { plugin, manifestPath, contributions, issues };
|
|
176
|
+
}
|
|
177
|
+
// Executable path safety + exec bit.
|
|
178
|
+
const execRel = parsed['executable'];
|
|
179
|
+
if (typeof execRel !== 'string' || execRel.length === 0) {
|
|
180
|
+
issue('command_manifest_invalid', `executable must be a non-empty string`, typeName(execRel), 'a relative path string', 'Set executable to the plugin-relative path of the command binary.', 'executable');
|
|
181
|
+
return { plugin, manifestPath, contributions, issues };
|
|
182
|
+
}
|
|
183
|
+
const execSafe = safePath(plugin.root, execRel);
|
|
184
|
+
if (execSafe === null) {
|
|
185
|
+
issue('command_path_unsafe', `executable escapes the plugin root or is not a regular file`, execRel, 'a relative path to a regular file inside the plugin root', 'Fix the executable path in commands.json.', 'executable');
|
|
186
|
+
return { plugin, manifestPath, contributions, issues };
|
|
187
|
+
}
|
|
188
|
+
if (!hasExecBit(execSafe)) {
|
|
189
|
+
issue('command_not_executable', `executable lacks the POSIX exec bit`, execRel, 'a file with an executable permission bit', 'chmod +x the plugin\'s command binary, then update the plugin.', 'executable');
|
|
190
|
+
return { plugin, manifestPath, contributions, issues };
|
|
191
|
+
}
|
|
192
|
+
// Mounts.
|
|
193
|
+
const mounts = parsed['mounts'];
|
|
194
|
+
if (!Array.isArray(mounts)) {
|
|
195
|
+
issue('command_manifest_invalid', `mounts must be an array`, typeName(mounts), 'an array of mounts', 'Declare at least one mount.', 'mounts');
|
|
196
|
+
return { plugin, manifestPath, executable: execSafe, contributions, issues };
|
|
197
|
+
}
|
|
198
|
+
if (mounts.length === 0) {
|
|
199
|
+
issue('command_manifest_invalid', `manifest declares no mounts (inert)`, '[]', 'at least one top-level mount', 'Declare a mount, or drop the `commands` field.', 'mounts');
|
|
200
|
+
}
|
|
201
|
+
const topNames = new Set();
|
|
202
|
+
mounts.forEach((mount, i) => {
|
|
203
|
+
const path = `mounts[${i}]`;
|
|
204
|
+
if (!isObject(mount)) {
|
|
205
|
+
issue('command_node_invalid', `mount must be an object`, typeName(mount), 'a { parent, node } object', 'Fix the mount.', path);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const mUnknown = Object.keys(mount).filter((k) => !MOUNT_KEYS.has(k));
|
|
209
|
+
if (mUnknown.length > 0) {
|
|
210
|
+
issue('command_node_invalid', `mount has unknown keys`, mUnknown.join(', '), 'only: parent, node', 'Remove the unknown keys.', path);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// v1: top-level mounts only — parent must be [].
|
|
214
|
+
if (!Array.isArray(mount['parent']) || mount['parent'].length !== 0) {
|
|
215
|
+
issue('command_parent_invalid', `mount parent must be [] (top-level only in v1)`, JSON.stringify(mount['parent']), 'an empty array []', 'Mount the node at top level; nested mounts are not supported in v1.', `${path}.parent`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const node = validateNode(mount['node'], `${path}.node`, true, issue);
|
|
219
|
+
if (node === null)
|
|
220
|
+
return;
|
|
221
|
+
// node is a top-level branch here (validateNode enforced kind + rootEntry).
|
|
222
|
+
if (topNames.has(node.name)) {
|
|
223
|
+
issue('command_node_invalid', `this plugin declares two top-level commands named "${node.name}"`, node.name, 'a unique top-level command name per plugin', 'Rename one of the duplicate top-level mounts.', `${path}.node.name`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
topNames.add(node.name);
|
|
227
|
+
if (reservedNames.has(node.name)) {
|
|
228
|
+
issue('command_collision', `top-level command "${node.name}" collides with a core command`, node.name, 'a name not used by a core crtr command', 'Rename the plugin\'s top-level command.', `${path}.node.name`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
contributions.push({ plugin, executable: execSafe, node: node });
|
|
232
|
+
});
|
|
233
|
+
return { plugin, manifestPath, executable: execSafe, contributions, issues };
|
|
234
|
+
}
|
|
235
|
+
/** Validate one declarative node. Returns the typed node on success, else null
|
|
236
|
+
* (recording issues). `topLevel` requires a branch with a rootEntry. */
|
|
237
|
+
function validateNode(raw, path, topLevel, issue) {
|
|
238
|
+
if (!isObject(raw)) {
|
|
239
|
+
issue('command_node_invalid', `node must be an object`, typeName(raw), 'a branch or leaf node object', 'Fix the node.', path);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const kind = raw['kind'];
|
|
243
|
+
if (kind === 'branch')
|
|
244
|
+
return validateBranch(raw, path, topLevel, issue);
|
|
245
|
+
if (kind === 'leaf') {
|
|
246
|
+
if (topLevel) {
|
|
247
|
+
issue('command_node_invalid', `top-level node must be a branch`, 'leaf', 'a branch node with a rootEntry', 'Wrap the command in a top-level branch (noun) with a rootEntry.', `${path}.kind`);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return validateLeaf(raw, path, issue);
|
|
251
|
+
}
|
|
252
|
+
issue('command_node_invalid', `node kind must be "branch" or "leaf"`, String(kind), 'branch | leaf', 'Set kind to branch or leaf.', `${path}.kind`);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function validateBranch(raw, path, topLevel, issue) {
|
|
256
|
+
if (!checkKeys(raw, BRANCH_KEYS, path, issue))
|
|
257
|
+
return null;
|
|
258
|
+
if (!checkCommon(raw, path, issue))
|
|
259
|
+
return null;
|
|
260
|
+
// rootEntry: required at top level, forbidden nested.
|
|
261
|
+
const hasRoot = raw['rootEntry'] !== undefined;
|
|
262
|
+
if (topLevel && !hasRoot) {
|
|
263
|
+
issue('command_node_invalid', `top-level branch requires a rootEntry`, '(missing)', 'a rootEntry { concept, description, whenToUse }', 'Add a rootEntry to the top-level branch.', `${path}.rootEntry`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (!topLevel && hasRoot) {
|
|
267
|
+
issue('command_node_invalid', `nested branch must not declare a rootEntry`, 'rootEntry', 'no rootEntry (only top-level branches carry one)', 'Remove rootEntry from the nested branch.', `${path}.rootEntry`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
let rootEntry;
|
|
271
|
+
if (hasRoot) {
|
|
272
|
+
const re = validateRootEntry(raw['rootEntry'], `${path}.rootEntry`, issue);
|
|
273
|
+
if (re === null)
|
|
274
|
+
return null;
|
|
275
|
+
rootEntry = re;
|
|
276
|
+
}
|
|
277
|
+
if (typeof raw['summary'] !== 'string' || raw['summary'].length === 0) {
|
|
278
|
+
issue('command_node_invalid', `branch summary must be a non-empty string`, typeName(raw['summary']), 'a one-line summary string', 'Add a summary.', `${path}.summary`);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (raw['model'] !== undefined && typeof raw['model'] !== 'string') {
|
|
282
|
+
issue('command_node_invalid', `branch model must be a string`, typeName(raw['model']), 'a string or omitted', 'Fix or remove model.', `${path}.model`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const children = raw['children'];
|
|
286
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
287
|
+
issue('command_node_invalid', `branch children must be a non-empty array`, typeName(children), 'a non-empty array of branch/leaf nodes', 'Add at least one child.', `${path}.children`);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const validated = [];
|
|
291
|
+
const childNames = new Set();
|
|
292
|
+
for (let i = 0; i < children.length; i++) {
|
|
293
|
+
const c = validateNode(children[i], `${path}.children[${i}]`, false, issue);
|
|
294
|
+
if (c === null)
|
|
295
|
+
return null;
|
|
296
|
+
if (childNames.has(c.name)) {
|
|
297
|
+
issue('command_node_invalid', `duplicate child name`, c.name, 'unique child names within a branch', 'Rename the duplicate child.', `${path}.children[${i}].name`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
childNames.add(c.name);
|
|
301
|
+
validated.push(c);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
kind: 'branch',
|
|
305
|
+
name: raw['name'],
|
|
306
|
+
description: raw['description'],
|
|
307
|
+
whenToUse: raw['whenToUse'],
|
|
308
|
+
...(raw['tier'] !== undefined ? { tier: raw['tier'] } : {}),
|
|
309
|
+
...(rootEntry !== undefined ? { rootEntry } : {}),
|
|
310
|
+
summary: raw['summary'],
|
|
311
|
+
...(raw['model'] !== undefined ? { model: raw['model'] } : {}),
|
|
312
|
+
children: validated,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function validateLeaf(raw, path, issue) {
|
|
316
|
+
if (!checkKeys(raw, LEAF_KEYS, path, issue))
|
|
317
|
+
return null;
|
|
318
|
+
if (!checkCommon(raw, path, issue))
|
|
319
|
+
return null;
|
|
320
|
+
if (typeof raw['summary'] !== 'string' || raw['summary'].length === 0) {
|
|
321
|
+
issue('command_node_invalid', `leaf summary must be a non-empty string`, typeName(raw['summary']), 'a one-line summary string', 'Add a summary.', `${path}.summary`);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (raw['outputKind'] !== 'object') {
|
|
325
|
+
issue('command_node_invalid', `leaf outputKind must be "object" in v1`, String(raw['outputKind']), 'exactly "object"', 'Set outputKind to "object".', `${path}.outputKind`);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const params = validateParams(raw['params'], `${path}.params`, issue);
|
|
329
|
+
if (params === null)
|
|
330
|
+
return null;
|
|
331
|
+
const output = validateOutput(raw['output'], `${path}.output`, issue);
|
|
332
|
+
if (output === null)
|
|
333
|
+
return null;
|
|
334
|
+
const effects = raw['effects'];
|
|
335
|
+
if (!Array.isArray(effects) || effects.length === 0 || !effects.every((e) => typeof e === 'string')) {
|
|
336
|
+
issue('command_node_invalid', `leaf effects must be a non-empty string array`, typeName(effects), 'a non-empty array of strings (["None. Read-only."] for read-only leaves)', 'Declare every persistent effect.', `${path}.effects`);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
kind: 'leaf',
|
|
341
|
+
name: raw['name'],
|
|
342
|
+
description: raw['description'],
|
|
343
|
+
whenToUse: raw['whenToUse'],
|
|
344
|
+
...(raw['tier'] !== undefined ? { tier: raw['tier'] } : {}),
|
|
345
|
+
summary: raw['summary'],
|
|
346
|
+
params,
|
|
347
|
+
output,
|
|
348
|
+
outputKind: 'object',
|
|
349
|
+
effects: effects,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** Validate name/description/whenToUse/tier shared by branches and leaves. */
|
|
353
|
+
function checkCommon(raw, path, issue) {
|
|
354
|
+
const name = raw['name'];
|
|
355
|
+
if (typeof name !== 'string' || !KEBAB.test(name)) {
|
|
356
|
+
issue('command_node_invalid', `node name must be a lowercase kebab-case token`, String(name), 'a kebab-case token like "app" or "list-all"', 'Rename the node.', `${path}.name`);
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (typeof raw['description'] !== 'string' || raw['description'].length === 0) {
|
|
360
|
+
issue('command_node_invalid', `node description must be a non-empty string`, typeName(raw['description']), 'a short parent-listing description', 'Add a description.', `${path}.description`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (typeof raw['whenToUse'] !== 'string' || raw['whenToUse'].length === 0) {
|
|
364
|
+
issue('command_node_invalid', `node whenToUse must be a non-empty string`, typeName(raw['whenToUse']), 'a parent selection rubric', 'Add a whenToUse rubric.', `${path}.whenToUse`);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (raw['tier'] !== undefined && !TIERS.has(String(raw['tier']))) {
|
|
368
|
+
issue('command_node_invalid', `node tier must be normal|common|important`, String(raw['tier']), 'normal | common | important', 'Fix or omit tier (external plugins cannot declare hidden commands).', `${path}.tier`);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
function validateRootEntry(raw, path, issue) {
|
|
374
|
+
if (!isObject(raw)) {
|
|
375
|
+
issue('command_node_invalid', `rootEntry must be an object`, typeName(raw), 'a { concept, description, whenToUse } object', 'Fix rootEntry.', path);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (!checkKeys(raw, ROOT_ENTRY_KEYS, path, issue))
|
|
379
|
+
return null;
|
|
380
|
+
for (const k of ['concept', 'description', 'whenToUse']) {
|
|
381
|
+
if (typeof raw[k] !== 'string' || raw[k].length === 0) {
|
|
382
|
+
issue('command_node_invalid', `rootEntry.${k} must be a non-empty string`, typeName(raw[k]), 'a non-empty string', `Add rootEntry.${k}.`, `${path}.${k}`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
concept: raw['concept'],
|
|
388
|
+
description: raw['description'],
|
|
389
|
+
whenToUse: raw['whenToUse'],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function validateParams(raw, path, issue) {
|
|
393
|
+
if (raw === undefined)
|
|
394
|
+
return [];
|
|
395
|
+
if (!Array.isArray(raw)) {
|
|
396
|
+
issue('command_node_invalid', `params must be an array`, typeName(raw), 'an array of params (or omitted)', 'Fix params.', path);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const out = [];
|
|
400
|
+
const names = new Set();
|
|
401
|
+
let positionals = 0;
|
|
402
|
+
for (let i = 0; i < raw.length; i++) {
|
|
403
|
+
const p = raw[i];
|
|
404
|
+
const pp = `${path}[${i}]`;
|
|
405
|
+
if (!isObject(p)) {
|
|
406
|
+
issue('command_node_invalid', `param must be an object`, typeName(p), 'a param object', 'Fix the param.', pp);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const kind = p['kind'];
|
|
410
|
+
if (typeof kind !== 'string' || !(kind in PARAM_KEYS_BY_KIND)) {
|
|
411
|
+
issue('command_node_invalid', `param kind must be positional|flag|stdin|context-file`, String(kind), 'positional | flag | stdin | context-file', 'Fix the param kind.', `${pp}.kind`);
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (!checkKeys(p, PARAM_KEYS_BY_KIND[kind], pp, issue))
|
|
415
|
+
return null;
|
|
416
|
+
const name = p['name'];
|
|
417
|
+
if (typeof name !== 'string' || !KEBAB.test(name)) {
|
|
418
|
+
issue('command_node_invalid', `param name must be a kebab-case token`, String(name), 'a kebab-case token', 'Rename the param.', `${pp}.name`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
if (names.has(name)) {
|
|
422
|
+
issue('command_node_invalid', `duplicate param name`, name, 'unique param names', 'Rename the duplicate param.', `${pp}.name`);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
names.add(name);
|
|
426
|
+
if (typeof p['required'] !== 'boolean') {
|
|
427
|
+
issue('command_node_invalid', `param required must be a boolean`, typeName(p['required']), 'true | false', 'Set required.', `${pp}.required`);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
if (typeof p['constraint'] !== 'string') {
|
|
431
|
+
issue('command_node_invalid', `param constraint must be a string`, typeName(p['constraint']), 'an inline constraint string', 'Add a constraint.', `${pp}.constraint`);
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (kind === 'positional') {
|
|
435
|
+
positionals++;
|
|
436
|
+
if (positionals > 1) {
|
|
437
|
+
issue('command_node_invalid', `a leaf may declare at most one positional`, 'multiple positionals', 'at most one positional param', 'Convert extra positionals to flags.', pp);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
out.push({ kind: 'positional', name, ...(p['type'] !== undefined ? { type: p['type'] } : {}), required: p['required'], constraint: p['constraint'] });
|
|
441
|
+
}
|
|
442
|
+
else if (kind === 'stdin') {
|
|
443
|
+
out.push({ kind: 'stdin', name, required: p['required'], constraint: p['constraint'] });
|
|
444
|
+
}
|
|
445
|
+
else if (kind === 'context-file') {
|
|
446
|
+
out.push({ kind: 'context-file', name, required: p['required'], constraint: p['constraint'], ...(typeof p['shape'] === 'string' ? { shape: p['shape'] } : {}) });
|
|
447
|
+
}
|
|
448
|
+
else if (kind === 'flag') {
|
|
449
|
+
const type = p['type'];
|
|
450
|
+
if (typeof type !== 'string' || !FLAG_TYPES.has(type)) {
|
|
451
|
+
issue('command_node_invalid', `flag type must be string|int|bool|path|enum`, String(type), 'string | int | bool | path | enum', 'Fix the flag type.', `${pp}.type`);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
let choices;
|
|
455
|
+
if (type === 'enum') {
|
|
456
|
+
const c = p['choices'];
|
|
457
|
+
if (!Array.isArray(c) || c.length === 0 || !c.every((x) => typeof x === 'string')) {
|
|
458
|
+
issue('command_node_invalid', `enum flag requires a non-empty string choices array`, typeName(c), 'a non-empty array of strings', 'Declare choices for the enum flag.', `${pp}.choices`);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
choices = c;
|
|
462
|
+
}
|
|
463
|
+
else if (p['choices'] !== undefined) {
|
|
464
|
+
issue('command_node_invalid', `choices is only valid on an enum flag`, 'choices', 'no choices unless type is enum', 'Remove choices.', `${pp}.choices`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
out.push({
|
|
468
|
+
kind: 'flag', name, type: type,
|
|
469
|
+
...(choices !== undefined ? { choices } : {}),
|
|
470
|
+
required: p['required'], constraint: p['constraint'],
|
|
471
|
+
...(p['default'] !== undefined ? { default: p['default'] } : {}),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
function validateOutput(raw, path, issue) {
|
|
478
|
+
if (!Array.isArray(raw)) {
|
|
479
|
+
issue('command_node_invalid', `output must be an array`, typeName(raw), 'an array of output fields', 'Declare the leaf output fields.', path);
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const out = [];
|
|
483
|
+
for (let i = 0; i < raw.length; i++) {
|
|
484
|
+
const f = raw[i];
|
|
485
|
+
const fp = `${path}[${i}]`;
|
|
486
|
+
if (!isObject(f)) {
|
|
487
|
+
issue('command_node_invalid', `output field must be an object`, typeName(f), 'a { name, type, required, constraint } object', 'Fix the output field.', fp);
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
if (!checkKeys(f, OUTPUT_KEYS, fp, issue))
|
|
491
|
+
return null;
|
|
492
|
+
if (typeof f['name'] !== 'string' || f['name'].length === 0) {
|
|
493
|
+
issue('command_node_invalid', `output field name must be a non-empty string`, typeName(f['name']), 'a field name', 'Add a name.', `${fp}.name`);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
if (typeof f['type'] !== 'string' || f['type'].length === 0) {
|
|
497
|
+
issue('command_node_invalid', `output field type must be a non-empty string`, typeName(f['type']), 'a type name', 'Add a type.', `${fp}.type`);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
if (typeof f['required'] !== 'boolean') {
|
|
501
|
+
issue('command_node_invalid', `output field required must be a boolean`, typeName(f['required']), 'true | false', 'Set required.', `${fp}.required`);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
if (typeof f['constraint'] !== 'string') {
|
|
505
|
+
issue('command_node_invalid', `output field constraint must be a string`, typeName(f['constraint']), 'a constraint string', 'Add a constraint.', `${fp}.constraint`);
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
out.push({ name: f['name'], type: f['type'], required: f['required'], constraint: f['constraint'] });
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Small helpers
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
function isObject(v) {
|
|
516
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
517
|
+
}
|
|
518
|
+
function typeName(v) {
|
|
519
|
+
if (v === null)
|
|
520
|
+
return 'null';
|
|
521
|
+
if (Array.isArray(v))
|
|
522
|
+
return 'array';
|
|
523
|
+
return typeof v;
|
|
524
|
+
}
|
|
525
|
+
function checkKeys(raw, allowed, path, issue) {
|
|
526
|
+
const unknown = Object.keys(raw).filter((k) => !allowed.has(k));
|
|
527
|
+
if (unknown.length > 0) {
|
|
528
|
+
issue('command_node_invalid', `unknown keys`, unknown.join(', '), `only: ${[...allowed].join(', ')}`, 'Remove the unknown keys.', path);
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
/** Resolve `rel` under `root`, confining it to the realpath'd root and
|
|
534
|
+
* requiring a regular file. Returns the resolved absolute path or null. */
|
|
535
|
+
function safePath(root, rel) {
|
|
536
|
+
if (typeof rel !== 'string' || rel.length === 0)
|
|
537
|
+
return null;
|
|
538
|
+
let rootReal;
|
|
539
|
+
let real;
|
|
540
|
+
try {
|
|
541
|
+
rootReal = realpathSync(root);
|
|
542
|
+
real = realpathSync(resolve(root, rel));
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep))
|
|
548
|
+
return null;
|
|
549
|
+
try {
|
|
550
|
+
if (!statSync(real).isFile())
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return real;
|
|
557
|
+
}
|
|
558
|
+
function hasExecBit(file) {
|
|
559
|
+
try {
|
|
560
|
+
return (statSync(file).mode & 0o111) !== 0;
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import type { Scope, ScopeConfig, ScopeState,
|
|
1
|
+
import type { Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig } from '../types.js';
|
|
2
2
|
export declare function configPath(scope: Scope): string | null;
|
|
3
3
|
export declare function statePath(scope: Scope): string | null;
|
|
4
|
-
export
|
|
5
|
-
readonly canvasNav: CanvasNavConfig;
|
|
6
|
-
};
|
|
7
|
-
export declare function readConfig(scope: Scope): ConsumerCompatibleScopeConfig;
|
|
4
|
+
export declare function readConfig(scope: Scope): ScopeConfig;
|
|
8
5
|
export declare function readState(scope: Scope): ScopeState;
|
|
9
6
|
type RawConfig = Record<string, unknown>;
|
|
10
7
|
export declare function updateRawConfigAtomically(scope: Scope, mutate: (latest: RawConfig) => RawConfig): RawConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { resolveViewPickAction } from '../../../commands/view-pick.js';
|
|
4
|
+
import { resolveBrowseNavAction, resolveBrowseSearchAction } from '../../canvas/browse/app.js';
|
|
5
|
+
import { resolveProfileMenuAction, resolveProfileSearchAction } from '../../profiles/select.js';
|
|
6
|
+
import { resolveKeybindings } from '../index.js';
|
|
7
|
+
test('bespoke consumers resolve defaults and replacements without retaining old gestures', () => {
|
|
8
|
+
const defaults = resolveKeybindings();
|
|
9
|
+
assert.equal(resolveViewPickAction(defaults, { input: 'j' }), 'crtr.view-picker.down');
|
|
10
|
+
assert.equal(resolveBrowseNavAction(defaults, { input: 'G' }), 'crtr.browse.last');
|
|
11
|
+
// Unit 1's terminal-space canonicalization lets a literal space key resolve the
|
|
12
|
+
// catalog `space` gesture — the browse fold toggle now flows through the catalog.
|
|
13
|
+
assert.equal(resolveBrowseNavAction(defaults, { input: ' ' }), 'crtr.browse.toggle-fold');
|
|
14
|
+
assert.equal(resolveBrowseSearchAction(defaults, { return: true }), 'crtr.browse.search.commit');
|
|
15
|
+
assert.equal(resolveProfileMenuAction(defaults, { input: '3' }), 'crtr.profile.menu.slot.3');
|
|
16
|
+
assert.equal(resolveProfileSearchAction(defaults, { input: 'p', ctrl: true }), 'crtr.profile.search.up');
|
|
17
|
+
const remapped = resolveKeybindings({
|
|
18
|
+
'crtr.view-picker.down': ['z'],
|
|
19
|
+
'crtr.browse.last': ['z'],
|
|
20
|
+
'crtr.browse.search.commit': ['z'],
|
|
21
|
+
'crtr.profile.menu.slot.3': ['z'],
|
|
22
|
+
'crtr.profile.search.up': ['z'],
|
|
23
|
+
});
|
|
24
|
+
assert.equal(remapped.diagnostics.length, 0);
|
|
25
|
+
assert.equal(resolveViewPickAction(remapped, { input: 'j' }), null);
|
|
26
|
+
assert.equal(resolveViewPickAction(remapped, { input: 'z' }), 'crtr.view-picker.down');
|
|
27
|
+
assert.equal(resolveBrowseNavAction(remapped, { input: 'G' }), null);
|
|
28
|
+
assert.equal(resolveBrowseNavAction(remapped, { input: 'z' }), 'crtr.browse.last');
|
|
29
|
+
assert.equal(resolveBrowseSearchAction(remapped, { return: true }), null);
|
|
30
|
+
assert.equal(resolveBrowseSearchAction(remapped, { input: 'z' }), 'crtr.browse.search.commit');
|
|
31
|
+
assert.equal(resolveProfileMenuAction(remapped, { input: '3' }), null);
|
|
32
|
+
assert.equal(resolveProfileMenuAction(remapped, { input: 'z' }), 'crtr.profile.menu.slot.3');
|
|
33
|
+
assert.equal(resolveProfileSearchAction(remapped, { input: 'p', ctrl: true }), null);
|
|
34
|
+
assert.equal(resolveProfileSearchAction(remapped, { input: 'z' }), 'crtr.profile.search.up');
|
|
35
|
+
});
|
|
36
|
+
test('profile search leaves printable filter payloads outside catalog actions', () => {
|
|
37
|
+
const bindings = resolveKeybindings();
|
|
38
|
+
assert.equal(resolveProfileSearchAction(bindings, { input: 'w' }), null);
|
|
39
|
+
assert.equal(resolveProfileSearchAction(bindings, { input: '文' }), null);
|
|
40
|
+
});
|
|
@@ -14,7 +14,7 @@ function ids(prefix, actions) {
|
|
|
14
14
|
return actions.map((action) => `${prefix}.${action}`);
|
|
15
15
|
}
|
|
16
16
|
const REQUIRED_IDS = [
|
|
17
|
-
...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'demote', 'detach', 'close-subtree', 'context', 'graph', 'focus-manager', 'issues']),
|
|
17
|
+
...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'demote', 'detach', 'close-subtree', 'context', 'providers', 'graph', 'focus-manager', 'issues']),
|
|
18
18
|
...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
|
|
19
19
|
...ids('crtr.tmux.node', ['next', 'previous']),
|
|
20
20
|
...ids('crtr.tmux.view', ['next', 'previous']),
|
|
@@ -32,7 +32,7 @@ const REQUIRED_IDS = [
|
|
|
32
32
|
...ids('crtr.view.settings', ['down', 'up', 'cell-previous', 'cell-next', 'refresh', 'quit', 'edit-or-submit', 'cancel-edit']),
|
|
33
33
|
...ids('crtr.view.workspace-sidebar', ['down', 'up', 'open', 'refresh', 'quit']),
|
|
34
34
|
...ids('crtr.view-picker', ['down', 'up', 'run', 'quit']),
|
|
35
|
-
...ids('crtr.browse', ['quit', 'down', 'up', 'first', 'last', 'expand-or-child', 'collapse-or-parent', 'toggle-fold', 'tab-next', 'tab-previous', 'tab.1', 'tab.2', 'tab.3', 'tab.4', 'close-subtree', 'confirm-close', 'sort', 'scope', 'residents', 'preview', 'search', 'revive-now', 'focus']),
|
|
35
|
+
...ids('crtr.browse', ['quit', 'down', 'up', 'first', 'last', 'expand-or-child', 'collapse-or-parent', 'toggle-fold', 'tab-next', 'tab-previous', 'tab.1', 'tab.2', 'tab.3', 'tab.4', 'tab.5', 'close-subtree', 'confirm-close', 'sort', 'scope', 'residents', 'preview', 'search', 'revive-now', 'focus']),
|
|
36
36
|
...ids('crtr.browse.search', ['cancel', 'commit', 'delete', 'quit']),
|
|
37
37
|
...ids('crtr.profile.menu', ['up', 'down', 'select', 'accept-default', 'abort', 'slot.1', 'slot.2', 'slot.3', 'slot.4', 'slot.5', 'slot.6', 'slot.7', 'slot.8', 'slot.9', 'create', 'search-existing', 'use-root', 'set-default', 'add-directory', 'unpin']),
|
|
38
38
|
...ids('crtr.profile.search', ['up', 'down', 'pick', 'back', 'delete', 'abort']),
|