@north-light/crouter 0.3.164 → 0.3.166
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 +6 -6
- package/dist/build-root.js +12 -13
- package/dist/builtin-memory/00-runtime-base.md +3 -4
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
- package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
- package/dist/builtin-memory/advisor/council.md +40 -0
- package/dist/builtin-memory/design.md +8 -11
- package/dist/builtin-memory/development.md +6 -9
- package/dist/builtin-memory/internal/INDEX.md +4 -5
- package/dist/builtin-memory/internal/agent-shaping.md +5 -7
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
- package/dist/builtin-memory/internal/marketplaces.md +13 -14
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +61 -45
- package/dist/builtin-memory/planning.md +4 -7
- package/dist/builtin-memory/spec.md +9 -12
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
- package/dist/cli.js +1 -1
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
- package/dist/clients/attach/__tests__/context-message.test.js +62 -22
- package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
- package/dist/clients/attach/render/chat-view.d.ts +20 -20
- package/dist/clients/attach/render/chat-view.js +40 -54
- package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
- package/dist/clients/attach/render/condensed-history.js +60 -0
- package/dist/clients/attach/render/context-message.d.ts +1 -1
- package/dist/clients/attach/render/context-message.js +18 -41
- package/dist/clients/attach/session/bindings.d.ts +5 -3
- package/dist/clients/attach/session/bindings.js +7 -13
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +36 -37
- package/dist/clients/attach/session/profile-files.d.ts +8 -0
- package/dist/clients/attach/session/profile-files.js +157 -0
- package/dist/clients/attach/viewer.js +528 -529
- package/dist/commands/node-context.js +3 -2
- package/dist/commands/node.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +6 -7
- package/dist/commands/pkg/plugin-manage.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +131 -19
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +6 -11
- package/dist/commands/profile/env.js +3 -3
- package/dist/commands/sys/config.js +17 -76
- package/dist/commands/sys/doctor.js +5 -91
- package/dist/commands/sys/setup-wizard.d.ts +9 -11
- package/dist/commands/sys/setup-wizard.js +47 -81
- package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
- package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
- package/dist/core/__tests__/command-plugins.test.js +75 -16
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -1
- package/dist/core/command-manifests/manifest.d.ts +24 -0
- package/dist/core/{configured-clis → command-manifests}/manifest.js +28 -25
- package/dist/core/command-manifests/registry.d.ts +1 -2
- package/dist/core/command-manifests/registry.js +2 -2
- package/dist/core/command-manifests/schema.d.ts +11 -13
- package/dist/core/command-manifests/schema.js +53 -192
- package/dist/core/command-plugins/compose.d.ts +0 -6
- package/dist/core/command-plugins/compose.js +32 -75
- package/dist/core/command-plugins/discovery.d.ts +25 -58
- package/dist/core/command-plugins/discovery.js +171 -261
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +48 -0
- package/dist/core/command-plugins/store.d.ts +16 -0
- package/dist/core/command-plugins/store.js +64 -0
- package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
- package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
- package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
- package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
- package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
- package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
- package/dist/core/command.d.ts +8 -9
- package/dist/core/command.js +6 -9
- package/dist/core/config.js +6 -10
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/io.d.ts +1 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
- package/dist/core/keybindings/attach-control.d.ts +37 -0
- package/dist/core/keybindings/attach-control.js +38 -0
- package/dist/core/keybindings/catalog.d.ts +5 -4
- package/dist/core/keybindings/catalog.js +16 -8
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/keybindings/types.d.ts +1 -1
- package/dist/core/preview-registry.js +41 -74
- package/dist/core/runtime/bearings.d.ts +2 -5
- package/dist/core/runtime/bearings.js +2 -5
- package/dist/core/runtime/broker.js +3 -2
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +4 -3
- package/dist/core/runtime/lifecycle.js +2 -2
- package/dist/core/runtime/situational-context.d.ts +1 -1
- package/dist/core/runtime/situational-context.js +1 -1
- package/dist/core/runtime/spawn.js +9 -9
- package/dist/core/runtime/tmux.js +29 -2
- package/dist/core/scope.d.ts +0 -5
- package/dist/core/scope.js +0 -10
- package/dist/core/user-settings.d.ts +193 -0
- package/dist/core/user-settings.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -2
- package/dist/prompts/review.js +4 -2
- package/dist/shared/generated-context.d.ts +34 -0
- package/dist/shared/generated-context.js +98 -0
- package/dist/types.d.ts +23 -16
- package/dist/types.js +3 -14
- package/dist/web-client/assets/index-BgLGlZ3D.css +2 -0
- package/dist/web-client/assets/{index-NIuSCOHM.js → index-CmoNqcCv.js} +19 -19
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
- package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
- package/dist/builtin-memory/internal/workflow-codification.md +0 -82
- package/dist/builtin-memory/product.md +0 -80
- package/dist/clients/attach/render/frozen-history.js +0 -100
- package/dist/commands/pkg/cli-inspect.d.ts +0 -17
- package/dist/commands/pkg/cli-inspect.js +0 -190
- package/dist/commands/pkg/cli-manage.d.ts +0 -3
- package/dist/commands/pkg/cli-manage.js +0 -206
- package/dist/commands/pkg/cli.d.ts +0 -1
- package/dist/commands/pkg/cli.js +0 -14
- package/dist/core/configured-clis/cache.d.ts +0 -16
- package/dist/core/configured-clis/cache.js +0 -57
- package/dist/core/configured-clis/compose.d.ts +0 -14
- package/dist/core/configured-clis/compose.js +0 -60
- package/dist/core/configured-clis/discovery.d.ts +0 -47
- package/dist/core/configured-clis/discovery.js +0 -173
- package/dist/core/configured-clis/manifest.d.ts +0 -24
- package/dist/core/configured-clis/registration.d.ts +0 -40
- package/dist/core/configured-clis/registration.js +0 -201
- package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// recoverable error propagation, and every malformed case.
|
|
19
19
|
// 5. Lifecycle without restart — install/disable/edit/remove all take effect
|
|
20
20
|
// on the next discovery call (per-invocation, no cache).
|
|
21
|
-
// 6/7. Typed issues for path/exec-bit/schema/unknown-key
|
|
22
|
-
// without executing the fixture binary.
|
|
21
|
+
// 6/7. Typed issues for path/exec-bit/schema/unknown-key violations and nested
|
|
22
|
+
// mount composition without executing the fixture binary.
|
|
23
23
|
import { test, describe, before, after, beforeEach } from 'node:test';
|
|
24
24
|
import assert from 'node:assert/strict';
|
|
25
25
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync } from 'node:fs';
|
|
@@ -30,6 +30,7 @@ import { join, dirname } from 'node:path';
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { discoverCommandContributions, effectiveCommandPlugins, validatePluginCommands, } from '../command-plugins/discovery.js';
|
|
32
32
|
import { composeExternalSubtrees, adaptPluginContributions } from '../command-plugins/compose.js';
|
|
33
|
+
import { resolveCommandRegistry } from '../command-manifests/registry.js';
|
|
33
34
|
import { defineRoot } from '../command.js';
|
|
34
35
|
import { renderRoot, renderBranch, renderLeafArgv } from '../help.js';
|
|
35
36
|
import { renderResult } from '../render.js';
|
|
@@ -69,7 +70,6 @@ process.stdin.on('end', () => {
|
|
|
69
70
|
function commandsJson(topName = 'app') {
|
|
70
71
|
return {
|
|
71
72
|
schemaVersion: 1,
|
|
72
|
-
executable: 'bin/cmd.js',
|
|
73
73
|
mounts: [
|
|
74
74
|
{
|
|
75
75
|
parent: [],
|
|
@@ -110,6 +110,28 @@ function commandsJson(topName = 'app') {
|
|
|
110
110
|
],
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
function passthroughCommandsJson() {
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
mounts: [{
|
|
117
|
+
parent: [],
|
|
118
|
+
node: {
|
|
119
|
+
kind: 'branch',
|
|
120
|
+
name: 'capture',
|
|
121
|
+
description: 'browser automation',
|
|
122
|
+
whenToUse: 'driving a browser through the Capture CLI',
|
|
123
|
+
rootEntry: {
|
|
124
|
+
concept: 'browser automation',
|
|
125
|
+
description: 'browser automation through Capture',
|
|
126
|
+
whenToUse: 'driving a browser through the Capture CLI',
|
|
127
|
+
},
|
|
128
|
+
summary: 'browser automation through Capture',
|
|
129
|
+
passthrough: { bin: 'capture', installHint: 'Install @crouton-kit/capture globally.' },
|
|
130
|
+
children: [],
|
|
131
|
+
},
|
|
132
|
+
}],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
113
135
|
/** Write a plugin into `scopeRoot/plugins/<name>/` with a command manifest +
|
|
114
136
|
* executable. `manifest` is the commands.json object (default fixture). */
|
|
115
137
|
function installPlugin(scopeRoot, name, opts = {}) {
|
|
@@ -117,7 +139,13 @@ function installPlugin(scopeRoot, name, opts = {}) {
|
|
|
117
139
|
mkdirSync(join(root, '.crouter-plugin'), { recursive: true });
|
|
118
140
|
mkdirSync(join(root, 'bin'), { recursive: true });
|
|
119
141
|
const pointer = opts.commandsPointer ?? 'commands.json';
|
|
120
|
-
writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({
|
|
142
|
+
writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({
|
|
143
|
+
name,
|
|
144
|
+
version: '0.1.0',
|
|
145
|
+
description: 'fixture',
|
|
146
|
+
commands: pointer,
|
|
147
|
+
transport: { kind: 'exec', ...(opts.executable === null ? {} : { executable: opts.executable ?? 'bin/cmd.js' }) },
|
|
148
|
+
}));
|
|
121
149
|
writeFileSync(join(root, 'commands.json'), JSON.stringify(opts.manifest ?? commandsJson()));
|
|
122
150
|
const execPath = join(root, 'bin', 'cmd.js');
|
|
123
151
|
writeFileSync(execPath, opts.exec ?? FIXTURE_EXEC);
|
|
@@ -229,12 +257,13 @@ describe('core collision', () => {
|
|
|
229
257
|
test('a fixture claiming a core top-level name is skipped + recorded', () => {
|
|
230
258
|
installPlugin(userRoot, 'rogue', { manifest: commandsJson('push') });
|
|
231
259
|
resetScopeCache();
|
|
232
|
-
const
|
|
233
|
-
const registrySnapshot = adaptPluginContributions(
|
|
260
|
+
const candidates = discoverCommandContributions(new Set(), emptyStart);
|
|
261
|
+
const registrySnapshot = resolveCommandRegistry(adaptPluginContributions(candidates.contributions).contributions, RESERVED);
|
|
234
262
|
assert.equal(composeExternalSubtrees(registrySnapshot).length, 0, 'the colliding branch must not enter the tree');
|
|
235
|
-
const collision =
|
|
263
|
+
const collision = registrySnapshot.issues.find((i) => i.code === 'command_collision');
|
|
236
264
|
assert.ok(collision, 'a command_collision issue must be recorded for doctor/show');
|
|
237
|
-
assert.equal(collision.
|
|
265
|
+
assert.equal(collision.path, 'push');
|
|
266
|
+
assert.equal(collision.contributor.name, 'rogue');
|
|
238
267
|
});
|
|
239
268
|
});
|
|
240
269
|
// ---------------------------------------------------------------------------
|
|
@@ -412,27 +441,57 @@ describe('typed validation issues', () => {
|
|
|
412
441
|
const v = validate(m);
|
|
413
442
|
assert.ok(v.issues.some((i) => i.code === 'command_manifest_invalid'));
|
|
414
443
|
});
|
|
415
|
-
test('non-empty parent
|
|
444
|
+
test('a child mount declared before its non-empty parent materializes into the nested branch', () => {
|
|
416
445
|
const m = commandsJson();
|
|
417
|
-
m.mounts[0]
|
|
446
|
+
const topLevel = m.mounts[0];
|
|
447
|
+
const child = topLevel.node['children'][0];
|
|
448
|
+
topLevel.node['children'] = [{
|
|
449
|
+
kind: 'branch',
|
|
450
|
+
name: 'tools',
|
|
451
|
+
description: 'application tools',
|
|
452
|
+
whenToUse: 'inspect application tools',
|
|
453
|
+
summary: 'application tools',
|
|
454
|
+
children: [],
|
|
455
|
+
}];
|
|
456
|
+
m.mounts = [
|
|
457
|
+
{ parent: ['app', 'tools'], node: { ...child, name: 'status' } },
|
|
458
|
+
topLevel,
|
|
459
|
+
];
|
|
418
460
|
const v = validate(m);
|
|
419
|
-
assert.
|
|
420
|
-
assert.equal(v.contributions.length,
|
|
461
|
+
assert.equal(v.issues.length, 0, JSON.stringify(v.issues));
|
|
462
|
+
assert.equal(v.contributions.length, 1);
|
|
463
|
+
const tools = (v.contributions[0].node.children.find((node) => node.name === 'tools'));
|
|
464
|
+
assert.equal(tools.kind, 'branch');
|
|
465
|
+
assert.deepEqual(tools.children.map((node) => node.name), ['status']);
|
|
421
466
|
});
|
|
422
467
|
test('non-executable file → command_not_executable', () => {
|
|
423
468
|
const v = validate(commandsJson(), { execBit: false });
|
|
424
469
|
assert.ok(v.issues.some((i) => i.code === 'command_not_executable'));
|
|
425
470
|
});
|
|
471
|
+
test('exec transport omits executable only for a passthrough-only manifest', () => {
|
|
472
|
+
const passthrough = validate(passthroughCommandsJson(), { executable: null });
|
|
473
|
+
assert.equal(passthrough.issues.length, 0, JSON.stringify(passthrough.issues));
|
|
474
|
+
assert.equal(passthrough.contributions.length, 1);
|
|
475
|
+
assert.equal(passthrough.contributions[0].node.passthrough?.bin, 'capture');
|
|
476
|
+
assert.equal(composeExternalSubtrees(adaptPluginContributions(passthrough.contributions))[0].passthrough?.bin, 'capture');
|
|
477
|
+
const nestedPassthrough = passthroughCommandsJson();
|
|
478
|
+
nestedPassthrough.mounts.push({
|
|
479
|
+
parent: ['capture'],
|
|
480
|
+
node: { kind: 'branch', name: 'child', description: 'nested child', whenToUse: 'testing invalid composition', summary: 'nested child', children: [] },
|
|
481
|
+
});
|
|
482
|
+
const rejectedNestedMount = validate(nestedPassthrough, { executable: null });
|
|
483
|
+
assert.ok(rejectedNestedMount.issues.some((issue) => issue.code === 'command_parent_invalid' && /passthrough branch/.test(issue.message)));
|
|
484
|
+
const executableLeaf = validate(commandsJson(), { executable: null });
|
|
485
|
+
assert.equal(executableLeaf.contributions.length, 0);
|
|
486
|
+
assert.ok(executableLeaf.issues.some((i) => i.path === 'transport.executable' && /requires executable/.test(i.message)));
|
|
487
|
+
});
|
|
426
488
|
test('executable escaping the plugin root → command_path_unsafe', () => {
|
|
427
|
-
const
|
|
428
|
-
m['executable'] = '../../../../bin/sh';
|
|
429
|
-
const v = validate(m);
|
|
489
|
+
const v = validate(commandsJson(), { executable: '../../../../bin/sh' });
|
|
430
490
|
assert.ok(v.issues.some((i) => i.code === 'command_path_unsafe'));
|
|
431
491
|
});
|
|
432
492
|
test('a top-level leaf (no rootEntry) → command_node_invalid', () => {
|
|
433
493
|
const m = {
|
|
434
494
|
schemaVersion: 1,
|
|
435
|
-
executable: 'bin/cmd.js',
|
|
436
495
|
mounts: [{ parent: [], node: { kind: 'leaf', name: 'x', description: 'd', whenToUse: 'w', summary: 's', params: [], output: [], outputKind: 'object', effects: ['None. Read-only.'] } }],
|
|
437
496
|
};
|
|
438
497
|
const v = validate(m);
|
|
@@ -46,10 +46,10 @@ test('base review ignores a light override and keeps its medium floor', () => {
|
|
|
46
46
|
test('no base kind defaults below the medium rung', () => {
|
|
47
47
|
const kinds = defaultKindsConfig();
|
|
48
48
|
for (const kind of [
|
|
49
|
-
'general', 'developer', 'design', 'plan', 'spec', '
|
|
49
|
+
'general', 'developer', 'design', 'plan', 'spec', 'review',
|
|
50
50
|
'plan/reviewers/requirements-coverage', 'plan/reviewers/pattern-consistency',
|
|
51
51
|
'plan/reviewers/code-smells', 'plan/reviewers/security', 'plan/reviewers/architecture-fit',
|
|
52
|
-
'spec/requirements',
|
|
52
|
+
'spec/requirements',
|
|
53
53
|
]) {
|
|
54
54
|
// Kind defaults are provider-qualified and differ per kind; what must hold
|
|
55
55
|
// for every one of these is the rung, not the provider or the concrete id.
|
|
@@ -24,7 +24,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
import { dirname, join, basename } from 'node:path';
|
|
25
25
|
import { ensureSession } from '../runtime/placement.js';
|
|
26
26
|
import { installTmuxBindings, legacyInboxKeyFile } from '../runtime/tmux-chrome.js';
|
|
27
|
-
import { resolveKeybindings } from '../keybindings/index.js';
|
|
27
|
+
import { ATTACH_CONTROL_BINDINGS, resolveKeybindings } from '../keybindings/index.js';
|
|
28
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
29
|
const SRC_ROOT = join(__dirname, '..', '..'); // .../src
|
|
30
30
|
const TMUX_TS = join(SRC_ROOT, 'core', 'runtime', 'tmux.ts');
|
|
@@ -214,6 +214,9 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
214
214
|
}
|
|
215
215
|
assert.ok(defaultMenu.indexOf('fork into idle root') > lifecycle && defaultMenu.indexOf('fork into idle root') < system, 'fork belongs in Lifecycle');
|
|
216
216
|
assert.match(defaultMenu, /"view settings ›" w .*display-menu.*fold finished tool calls/);
|
|
217
|
+
assert.match(defaultMenu, /"attach ›" a .*display-menu.*search profile files/);
|
|
218
|
+
assert.match(defaultMenu, /send-keys -l/);
|
|
219
|
+
assert.match(defaultMenu, /crtr\.attach\.profile-files/, 'the menu path must work while the profile-files direct binding is disabled by default');
|
|
217
220
|
// Matched with its selector, so the Explore group's "view settings ›" item
|
|
218
221
|
// (selector `w`) cannot satisfy this assertion by substring.
|
|
219
222
|
assert.ok(defaultMenu.indexOf('settings p ') > system, 'settings belongs in System');
|
|
@@ -221,6 +224,12 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
221
224
|
assert.match(tmuxBinding(socket, 'root', 'M-v') ?? '', /switch-client/);
|
|
222
225
|
// The inbox toggle installs at its default key (alt+i → M-i) in the same sweep.
|
|
223
226
|
assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /hl inbox toggle/);
|
|
227
|
+
const attachMenuDisabled = Object.fromEntries(ATTACH_CONTROL_BINDINGS.map(({ menuId }) => [menuId, []]));
|
|
228
|
+
const withoutAttach = installTmuxBindings(resolveKeybindings(attachMenuDisabled));
|
|
229
|
+
assert.equal(withoutAttach.ok, true, JSON.stringify(withoutAttach.diagnostics));
|
|
230
|
+
const menuWithoutAttach = tmuxBinding(socket, 'root', 'M-c') ?? '';
|
|
231
|
+
assert.doesNotMatch(menuWithoutAttach, /attach ›/, 'an empty attach submenu must hide its opener');
|
|
232
|
+
assert.doesNotMatch(menuWithoutAttach, /search profile files/);
|
|
224
233
|
const remapped = installTmuxBindings(resolveKeybindings({
|
|
225
234
|
'crtr.tmux.menu.open': ['alt+shift+z'],
|
|
226
235
|
'crtr.tmux.menu.graph': ['shift+z'],
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeclLeaf, TransportKind, DeclBranch, ManifestTimeouts, CommandManifestIssue } from './schema.js';
|
|
2
|
+
export interface ValidatedCommandManifest {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
timeouts?: ManifestTimeouts;
|
|
6
|
+
/** Top-level branches, each ready to mount at parent []. */
|
|
7
|
+
roots: DeclBranch<DeclLeaf>[];
|
|
8
|
+
}
|
|
9
|
+
export interface CommandManifestValidation {
|
|
10
|
+
/** The validated, materialized manifest (present only if all issues are fixed). */
|
|
11
|
+
manifest?: ValidatedCommandManifest;
|
|
12
|
+
/** All typed validation issues. Any issue means zero contributions. */
|
|
13
|
+
issues: CommandManifestIssue[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates a HTTP-transport plugin manifest exactly per §4, performing strict atomic
|
|
17
|
+
* validation and order-independent forest materialization. Returns either a
|
|
18
|
+
* valid manifest with roots ready to mount, or a complete issues list with
|
|
19
|
+
* zero contributions.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateCommandManifest(raw: unknown, options: {
|
|
22
|
+
transport: TransportKind;
|
|
23
|
+
reservedCoreNames: ReadonlySet<string>;
|
|
24
|
+
}): CommandManifestValidation;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Transport-parameterized command manifest validation and forest materialization.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Validates command manifests atomically, materializes self-contained forests
|
|
4
|
+
// independent of nested-mount order when the parent is inline, validates REST
|
|
5
|
+
// mappings, and enforces the canonical coalesced-body-source rule (exactly one
|
|
6
|
+
// stdin + one non-stdin flag/positional sharing a body `as`).
|
|
7
7
|
//
|
|
8
8
|
// This is pure validation: no fetch, no registration lookup, no file I/O beyond
|
|
9
9
|
// the raw manifest bytes passed in.
|
|
10
|
-
import {
|
|
10
|
+
import { validateCommandNode } from './schema.js';
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Validation helpers
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -27,27 +27,29 @@ function typeName(v) {
|
|
|
27
27
|
// Top-level manifest validation
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
/**
|
|
30
|
-
* Validates a
|
|
30
|
+
* Validates a HTTP-transport plugin manifest exactly per §4, performing strict atomic
|
|
31
31
|
* validation and order-independent forest materialization. Returns either a
|
|
32
32
|
* valid manifest with roots ready to mount, or a complete issues list with
|
|
33
33
|
* zero contributions.
|
|
34
34
|
*/
|
|
35
|
-
export function
|
|
35
|
+
export function validateCommandManifest(raw, options) {
|
|
36
36
|
const issues = [];
|
|
37
37
|
const issue = (code, message, received, expected, next, path) => {
|
|
38
38
|
issues.push({ code, message, received, expected, next, ...(path ? { path } : {}) });
|
|
39
39
|
};
|
|
40
40
|
// Top-level structure check
|
|
41
41
|
if (!isObject(raw)) {
|
|
42
|
-
issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), '{ schemaVersion, baseUrl?, timeouts?, mounts }', 'Provide a valid JSON manifest object.');
|
|
42
|
+
issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts }' : '{ schemaVersion, mounts }', 'Provide a valid JSON manifest object.');
|
|
43
43
|
return { issues };
|
|
44
44
|
}
|
|
45
45
|
// Check for unknown top-level keys
|
|
46
46
|
const topKeys = Object.keys(raw);
|
|
47
|
-
const allowedTopKeys =
|
|
47
|
+
const allowedTopKeys = options.transport === 'http'
|
|
48
|
+
? new Set(['schemaVersion', 'baseUrl', 'timeouts', 'mounts'])
|
|
49
|
+
: new Set(['schemaVersion', 'mounts']);
|
|
48
50
|
const unknownKeys = topKeys.filter((k) => !allowedTopKeys.has(k));
|
|
49
51
|
if (unknownKeys.length > 0) {
|
|
50
|
-
issue('command_manifest_invalid', `unknown top-level keys`, unknownKeys.join(', '), 'only: schemaVersion, baseUrl, timeouts, mounts', 'Remove the unknown keys.');
|
|
52
|
+
issue('command_manifest_invalid', `unknown top-level keys`, unknownKeys.join(', '), options.transport === 'http' ? 'only: schemaVersion, baseUrl, timeouts, mounts' : 'only: schemaVersion, mounts', 'Remove the unknown keys.');
|
|
51
53
|
return { issues };
|
|
52
54
|
}
|
|
53
55
|
// Validate schemaVersion
|
|
@@ -57,7 +59,7 @@ export function validateConfiguredCliManifest(raw, options) {
|
|
|
57
59
|
}
|
|
58
60
|
// Validate optional baseUrl
|
|
59
61
|
let baseUrl;
|
|
60
|
-
if (raw['baseUrl'] !== undefined) {
|
|
62
|
+
if (options.transport === 'http' && raw['baseUrl'] !== undefined) {
|
|
61
63
|
if (typeof raw['baseUrl'] !== 'string') {
|
|
62
64
|
issue('command_manifest_invalid', 'baseUrl must be a string', typeName(raw['baseUrl']), 'an absolute HTTP(S) URL or omitted', 'Provide a valid baseUrl or remove it.', 'baseUrl');
|
|
63
65
|
return { issues };
|
|
@@ -77,7 +79,7 @@ export function validateConfiguredCliManifest(raw, options) {
|
|
|
77
79
|
}
|
|
78
80
|
// Validate optional timeouts
|
|
79
81
|
let timeouts;
|
|
80
|
-
if (raw['timeouts'] !== undefined) {
|
|
82
|
+
if (options.transport === 'http' && raw['timeouts'] !== undefined) {
|
|
81
83
|
const t = validateTimeouts(raw['timeouts'], issue);
|
|
82
84
|
if (t === null)
|
|
83
85
|
return { issues };
|
|
@@ -96,7 +98,7 @@ export function validateConfiguredCliManifest(raw, options) {
|
|
|
96
98
|
// Validate and collect all mounts
|
|
97
99
|
const validatedMounts = [];
|
|
98
100
|
for (let i = 0; i < mounts.length; i++) {
|
|
99
|
-
const m = validateMount(mounts[i], i, issue);
|
|
101
|
+
const m = validateMount(mounts[i], i, options.transport, issue);
|
|
100
102
|
if (m === null)
|
|
101
103
|
return { issues };
|
|
102
104
|
validatedMounts.push(m);
|
|
@@ -146,7 +148,7 @@ function validateTimeouts(raw, issue) {
|
|
|
146
148
|
}
|
|
147
149
|
return out;
|
|
148
150
|
}
|
|
149
|
-
function validateMount(raw, index, issue) {
|
|
151
|
+
function validateMount(raw, index, transport, issue) {
|
|
150
152
|
const path = `mounts[${index}]`;
|
|
151
153
|
if (!isObject(raw)) {
|
|
152
154
|
issue('command_manifest_invalid', 'mount must be an object', typeName(raw), '{ parent, node }', 'Fix the mount.', path);
|
|
@@ -175,7 +177,7 @@ function validateMount(raw, index, issue) {
|
|
|
175
177
|
// Validate node
|
|
176
178
|
const nodeRaw = raw['node'];
|
|
177
179
|
const topLevel = parent.length === 0;
|
|
178
|
-
const node =
|
|
180
|
+
const node = validateCommandNode(nodeRaw, [`${path}.node`], topLevel, transport, issue);
|
|
179
181
|
if (node === null)
|
|
180
182
|
return null;
|
|
181
183
|
// Top-level mount must be a branch with rootEntry
|
|
@@ -183,7 +185,7 @@ function validateMount(raw, index, issue) {
|
|
|
183
185
|
issue('command_node_invalid', 'top-level mount node must be a branch', node.kind, 'a branch with a rootEntry', 'Wrap the command in a top-level branch.', `${path}.node.kind`);
|
|
184
186
|
return null;
|
|
185
187
|
}
|
|
186
|
-
// Ensure the node is properly typed (should only be
|
|
188
|
+
// Ensure the node is properly typed (should only be DeclLeaf at this point)
|
|
187
189
|
const typedNode = node;
|
|
188
190
|
return { parent, node: typedNode };
|
|
189
191
|
}
|
|
@@ -192,8 +194,7 @@ function validateMount(raw, index, issue) {
|
|
|
192
194
|
// ---------------------------------------------------------------------------
|
|
193
195
|
/**
|
|
194
196
|
* Materializes a flat list of mounts into a forest of top-level branches,
|
|
195
|
-
* resolving parent
|
|
196
|
-
* (no external parents), no duplicates, and no childless branches.
|
|
197
|
+
* resolving nested mounts whose parent exists in a top-level inline tree regardless of order. Nested mounts that provide another nested mount's parent must appear first. Validates self-containment and no duplicates.
|
|
197
198
|
*/
|
|
198
199
|
function materializeForest(mounts, reservedCoreNames, issue) {
|
|
199
200
|
// Separate top-level (parent: []) from nested (parent non-empty)
|
|
@@ -202,7 +203,7 @@ function materializeForest(mounts, reservedCoreNames, issue) {
|
|
|
202
203
|
// Check for core-name collisions in top-level nodes
|
|
203
204
|
for (const m of topLevel) {
|
|
204
205
|
if (reservedCoreNames.has(m.node.name)) {
|
|
205
|
-
issue('
|
|
206
|
+
issue('command_collision', `node name conflicts with a crtr core command`, m.node.name, `a name not in core (not one of: ${[...reservedCoreNames].join(', ')})`, 'Rename the node or remove this mount.', `${m.node.name}`);
|
|
206
207
|
return null;
|
|
207
208
|
}
|
|
208
209
|
}
|
|
@@ -216,6 +217,7 @@ function materializeForest(mounts, reservedCoreNames, issue) {
|
|
|
216
217
|
...(m.node.rootEntry !== undefined ? { rootEntry: m.node.rootEntry } : {}),
|
|
217
218
|
summary: m.node.summary,
|
|
218
219
|
...(m.node.model !== undefined ? { model: m.node.model } : {}),
|
|
220
|
+
...(m.node.passthrough !== undefined ? { passthrough: m.node.passthrough } : {}),
|
|
219
221
|
children: [...m.node.children],
|
|
220
222
|
}));
|
|
221
223
|
// Attach nested mounts
|
|
@@ -269,6 +271,12 @@ function resolveAndAttachMount(roots, mount, issue) {
|
|
|
269
271
|
}
|
|
270
272
|
current = child;
|
|
271
273
|
}
|
|
274
|
+
// A passthrough owns every token below its path, so nested mounts would make
|
|
275
|
+
// the materialized branch invalid even though its inline children are empty.
|
|
276
|
+
if (current.passthrough !== undefined) {
|
|
277
|
+
issue('command_parent_invalid', 'parent path resolves to a passthrough branch', mount.parent.join('.'), 'a branch without passthrough', 'Remove the nested mount or remove passthrough from its parent branch.', mount.parent.join('.'));
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
272
280
|
// Attach the node
|
|
273
281
|
if (current.children.some((c) => c.name === mount.node.name)) {
|
|
274
282
|
issue('command_node_invalid', `duplicate child name`, mount.node.name, 'unique child names within a branch', 'Rename the node or remove the duplicate.', `${mount.parent.join('.')}.${mount.node.name}`);
|
|
@@ -277,8 +285,3 @@ function resolveAndAttachMount(roots, mount, issue) {
|
|
|
277
285
|
current.children.push(mount.node);
|
|
278
286
|
return true;
|
|
279
287
|
}
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
// Configured CLI node validator (implements §4.3 + §4.4)
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
// Re-export and use the schema validator
|
|
284
|
-
export { validateConfiguredCliNode } from '../command-manifests/schema.js';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { LeafDef } from '../command.js';
|
|
2
2
|
import type { DeclLeafBase, DeclBranch, CommandManifestIssue } from './schema.js';
|
|
3
|
-
/** Reference to an external contributor (plugin or
|
|
3
|
+
/** Reference to an external contributor (plugin or HTTP-transport plugin). */
|
|
4
4
|
export type CommandContributorRef = {
|
|
5
|
-
kind: 'plugin' | 'configured-cli';
|
|
6
5
|
name: string;
|
|
7
6
|
scope: 'user' | 'project';
|
|
8
7
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Unified command registry: transport-neutral collision resolution and
|
|
2
2
|
// contributor attribution. This module takes raw candidates (from plugins,
|
|
3
|
-
//
|
|
3
|
+
// HTTP-transport plugins, or any external source) and runs one unified pass that:
|
|
4
4
|
//
|
|
5
5
|
// 1. Detects collisions by resolved path (shortest collision wins).
|
|
6
6
|
// 2. Drops all claimants at each collided path.
|
|
@@ -63,7 +63,7 @@ export function resolveCommandRegistry(candidates, reservedCoreNames) {
|
|
|
63
63
|
if (collisions.has(pathKey)) {
|
|
64
64
|
const collision = collisions.get(pathKey);
|
|
65
65
|
const claimantNames = collision.claimants.map((c) => c.contributor.name).join(', ');
|
|
66
|
-
const claimantKinds = collision.claimants.map((c) =>
|
|
66
|
+
const claimantKinds = collision.claimants.map((c) => c.contributor.name).join(', ');
|
|
67
67
|
// Record an issue on this claimant. A core collision has exactly one
|
|
68
68
|
// claimant contesting a reserved built-in name, not "multiple contributors".
|
|
69
69
|
issues.push(collision.isCoreCollision
|
|
@@ -14,10 +14,10 @@ export interface DeclBranch<L = DeclLeafBase> {
|
|
|
14
14
|
rootEntry?: DeclRootEntry;
|
|
15
15
|
summary: string;
|
|
16
16
|
model?: string;
|
|
17
|
-
/**
|
|
17
|
+
/** Exec transport only: forward every argv token after this branch to an
|
|
18
18
|
* external binary instead of parsing children. A passthrough branch is
|
|
19
|
-
* childless by construction.
|
|
20
|
-
*
|
|
19
|
+
* childless by construction. HTTP manifests reject passthrough because an
|
|
20
|
+
* HTTP transport must not name a local binary to execute. */
|
|
21
21
|
passthrough?: DeclPassthrough;
|
|
22
22
|
children: DeclNode<L>[];
|
|
23
23
|
}
|
|
@@ -36,11 +36,11 @@ export interface DeclLeafBase {
|
|
|
36
36
|
output: Field[];
|
|
37
37
|
effects: string[];
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
export interface
|
|
39
|
+
/** Exec-transport leaf: requires outputKind: 'object'. */
|
|
40
|
+
export interface ExecDeclLeaf extends DeclLeafBase {
|
|
41
41
|
outputKind: 'object';
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
43
|
+
/** HTTP-transport plugin REST mapping (placeholder types; full spec in manifest.ts). */
|
|
44
44
|
export type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
45
45
|
export type RestParamPlacement = 'path' | 'query' | 'body' | 'header';
|
|
46
46
|
export interface RestParamMapping {
|
|
@@ -64,11 +64,11 @@ export interface ManifestTimeouts {
|
|
|
64
64
|
requestMs?: number;
|
|
65
65
|
streamIdleMs?: number;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
export interface
|
|
67
|
+
/** HTTP-transport leaf: requires rest (derives outputKind from streaming). */
|
|
68
|
+
export interface HttpDeclLeaf extends DeclLeafBase {
|
|
69
69
|
rest: RestMapping;
|
|
70
70
|
}
|
|
71
|
-
export type DeclLeaf =
|
|
71
|
+
export type DeclLeaf = ExecDeclLeaf | HttpDeclLeaf;
|
|
72
72
|
export type DeclNode<L = DeclLeafBase> = DeclBranch<L> | (L extends DeclLeafBase ? DeclLeaf : never);
|
|
73
73
|
export interface CommandManifestIssue {
|
|
74
74
|
code: CommandIssueCode;
|
|
@@ -80,8 +80,6 @@ export interface CommandManifestIssue {
|
|
|
80
80
|
}
|
|
81
81
|
export type CommandIssueCode = 'command_manifest_unreadable' | 'command_manifest_invalid' | 'command_schema_version' | 'command_path_unsafe' | 'command_not_executable' | 'command_parent_invalid' | 'command_node_invalid' | 'command_collision' | 'command_rest_invalid';
|
|
82
82
|
type IssueFn = (code: CommandIssueCode, message: string, received: string, expected: string, next: string, path?: string) => void;
|
|
83
|
-
|
|
84
|
-
export declare function
|
|
85
|
-
/** Validate a node for the configured CLI dialect (leaf must have rest). */
|
|
86
|
-
export declare function validateConfiguredCliNode(raw: unknown, path: string, topLevel: boolean, issue: IssueFn): DeclBranch<ConfiguredCliDeclLeaf> | ConfiguredCliDeclLeaf | null;
|
|
83
|
+
export type TransportKind = 'exec' | 'http';
|
|
84
|
+
export declare function validateCommandNode(raw: unknown, path: string[], topLevel: boolean, transport: TransportKind, issue: IssueFn): DeclBranch<DeclLeaf> | DeclLeaf | null;
|
|
87
85
|
export {};
|