@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
import { createStatusLineCommandModule } from '../statusline-command-module.js';
|
|
5
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
6
|
+
|
|
7
|
+
const COMMAND_CONTEXT = createTestCommandHost();
|
|
8
|
+
|
|
9
|
+
describe('createStatusLineCommandModule', () => {
|
|
10
|
+
it('provides statusline metadata and user-only executable command from one module owner', () => {
|
|
11
|
+
const module = createStatusLineCommandModule();
|
|
12
|
+
const command = module.systemCommands?.[0];
|
|
13
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
14
|
+
|
|
15
|
+
expect(module.name).toBe('agent-command-statusline');
|
|
16
|
+
expect(entry).toEqual(
|
|
17
|
+
expect.objectContaining({
|
|
18
|
+
name: 'statusline',
|
|
19
|
+
description:
|
|
20
|
+
'Configure TUI status-line visibility and fields such as model, context, tokens, session, and git branch.',
|
|
21
|
+
source: 'statusline',
|
|
22
|
+
argumentHint: 'on | off | reset | git on | git off',
|
|
23
|
+
modelInvocable: false,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
expect(entry?.subcommands?.map((subcommand) => subcommand.name)).toEqual([
|
|
27
|
+
'on',
|
|
28
|
+
'off',
|
|
29
|
+
'reset',
|
|
30
|
+
'git',
|
|
31
|
+
]);
|
|
32
|
+
expect(command).toEqual(
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
name: 'statusline',
|
|
35
|
+
lifecycle: 'inline',
|
|
36
|
+
userInvocable: true,
|
|
37
|
+
modelInvocable: false,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
{
|
|
44
|
+
args: 'on',
|
|
45
|
+
message: 'Status line enabled.',
|
|
46
|
+
patch: { enabled: true },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
args: 'off',
|
|
50
|
+
message: 'Status line disabled.',
|
|
51
|
+
patch: { enabled: false },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
args: 'reset',
|
|
55
|
+
message: 'Status line settings reset.',
|
|
56
|
+
patch: { enabled: true, gitBranch: true },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
args: 'git on',
|
|
60
|
+
message: 'Status line git branch shown.',
|
|
61
|
+
patch: { gitBranch: true },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
args: 'git off',
|
|
65
|
+
message: 'Status line git branch hidden.',
|
|
66
|
+
patch: { gitBranch: false },
|
|
67
|
+
},
|
|
68
|
+
])(
|
|
69
|
+
'emits a statusline settings patch for /statusline $args',
|
|
70
|
+
async ({ args, message, patch }) => {
|
|
71
|
+
const executor = new SystemCommandExecutor([
|
|
72
|
+
...(createStatusLineCommandModule().systemCommands ?? []),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const result = await executor.execute('statusline', COMMAND_CONTEXT, args);
|
|
76
|
+
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
success: true,
|
|
79
|
+
message,
|
|
80
|
+
hostActions: [{ type: 'statusline-settings-patch', patch }],
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
it('reports usage for unsupported arguments without host actions', async () => {
|
|
86
|
+
const executor = new SystemCommandExecutor([
|
|
87
|
+
...(createStatusLineCommandModule().systemCommands ?? []),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const result = await executor.execute('statusline', COMMAND_CONTEXT, 'wat');
|
|
91
|
+
|
|
92
|
+
expect(result?.success).toBe(false);
|
|
93
|
+
expect(result?.message).toContain('Usage: /statusline');
|
|
94
|
+
expect(result?.hostActions).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildStatusLineCommandSubcommands,
|
|
3
|
+
STATUSLINE_COMMAND_ARGUMENT_HINT,
|
|
4
|
+
STATUSLINE_COMMAND_DESCRIPTION,
|
|
5
|
+
} from '@robota-sdk/agent-framework';
|
|
6
|
+
|
|
7
|
+
import { executeStatusLineCommand } from './statusline-command.js';
|
|
8
|
+
|
|
9
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
10
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
11
|
+
|
|
12
|
+
export function createStatusLineCommandEntry(): ICommand {
|
|
13
|
+
return {
|
|
14
|
+
name: 'statusline',
|
|
15
|
+
displayName: 'Status Line',
|
|
16
|
+
description: STATUSLINE_COMMAND_DESCRIPTION,
|
|
17
|
+
source: 'statusline',
|
|
18
|
+
argumentHint: STATUSLINE_COMMAND_ARGUMENT_HINT,
|
|
19
|
+
subcommands: buildStatusLineCommandSubcommands('statusline'),
|
|
20
|
+
// User-only: UI preference.
|
|
21
|
+
modelInvocable: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createStatusLineSystemCommand(): ISystemCommand {
|
|
26
|
+
const entry = createStatusLineCommandEntry();
|
|
27
|
+
return {
|
|
28
|
+
name: entry.name,
|
|
29
|
+
displayName: entry.displayName,
|
|
30
|
+
description: entry.description,
|
|
31
|
+
requiresPermission: false,
|
|
32
|
+
userInvocable: true,
|
|
33
|
+
modelInvocable: false,
|
|
34
|
+
argumentHint: entry.argumentHint,
|
|
35
|
+
subcommands: entry.subcommands,
|
|
36
|
+
lifecycle: 'inline',
|
|
37
|
+
execute: executeStatusLineCommand,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class StatusLineCommandSource implements ICommandSource {
|
|
42
|
+
readonly name = 'statusline';
|
|
43
|
+
|
|
44
|
+
getCommands(): ICommand[] {
|
|
45
|
+
return [createStatusLineCommandEntry()];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createStatusLineCommandModule(): ICommandModule {
|
|
50
|
+
return {
|
|
51
|
+
name: 'agent-command-statusline',
|
|
52
|
+
commandSources: [new StatusLineCommandSource()],
|
|
53
|
+
systemCommands: [createStatusLineSystemCommand()],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { DEFAULT_STATUS_LINE_COMMAND_SETTINGS } from '@robota-sdk/agent-framework';
|
|
2
|
+
|
|
3
|
+
import type { ICommandHostNoCapability } from '@robota-sdk/agent-framework';
|
|
4
|
+
import type {
|
|
5
|
+
ICommandResult,
|
|
6
|
+
TStatusLineCommandSettingsPatch,
|
|
7
|
+
} from '@robota-sdk/agent-interface-command';
|
|
8
|
+
|
|
9
|
+
interface IStatusLineCommandSuccessAction {
|
|
10
|
+
success: true;
|
|
11
|
+
message: string;
|
|
12
|
+
patch: TStatusLineCommandSettingsPatch;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IStatusLineCommandFailureAction {
|
|
16
|
+
success: false;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type TStatusLineCommandAction = IStatusLineCommandSuccessAction | IStatusLineCommandFailureAction;
|
|
21
|
+
|
|
22
|
+
export const STATUSLINE_USAGE = [
|
|
23
|
+
'Usage: /statusline on | off | reset | git on | git off',
|
|
24
|
+
'Fields: model, context, permission mode, message count, session name, thinking state, git branch.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
function parseStatusLineArgs(args: string): TStatusLineCommandAction {
|
|
28
|
+
const parts = args
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.filter((part) => part.length > 0);
|
|
33
|
+
const [first, second] = parts;
|
|
34
|
+
|
|
35
|
+
if (first === 'on' && second === undefined) {
|
|
36
|
+
return { success: true, message: 'Status line enabled.', patch: { enabled: true } };
|
|
37
|
+
}
|
|
38
|
+
if (first === 'off' && second === undefined) {
|
|
39
|
+
return { success: true, message: 'Status line disabled.', patch: { enabled: false } };
|
|
40
|
+
}
|
|
41
|
+
if (first === 'reset' && second === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
message: 'Status line settings reset.',
|
|
45
|
+
patch: { ...DEFAULT_STATUS_LINE_COMMAND_SETTINGS },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (first === 'git' && second === 'on' && parts.length === 2) {
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
message: 'Status line git branch shown.',
|
|
52
|
+
patch: { gitBranch: true },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (first === 'git' && second === 'off' && parts.length === 2) {
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
message: 'Status line git branch hidden.',
|
|
59
|
+
patch: { gitBranch: false },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { success: false, message: STATUSLINE_USAGE };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function executeStatusLineCommand(
|
|
67
|
+
_context: ICommandHostNoCapability,
|
|
68
|
+
args: string,
|
|
69
|
+
): ICommandResult {
|
|
70
|
+
const action = parseStatusLineArgs(args);
|
|
71
|
+
if (!action.success) {
|
|
72
|
+
return { success: false, message: action.message };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
message: action.message,
|
|
78
|
+
hostActions: [{ type: 'statusline-settings-patch', patch: action.patch }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3189 — the commands a terminal attached to a workspace daemon runs in its own process.
|
|
3
|
+
*
|
|
4
|
+
* The attached terminal routes by this set, not by the daemon's catalog, so the set must name exactly
|
|
5
|
+
* the commands the default assembly marks `runner: 'client'` — a command in one list and not the
|
|
6
|
+
* other would either run on the daemon or never run at all. And each client command must answer as
|
|
7
|
+
* the in-process command does, because it is the same command running somewhere else.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { createDefaultCommandModules } from '../../default/index.js';
|
|
16
|
+
import { createEditorCommandModule } from '../../editor/index.js';
|
|
17
|
+
import { createKeybindingsCommandModule } from '../../keybindings/index.js';
|
|
18
|
+
import { createShellCommandModule } from '../../shell/index.js';
|
|
19
|
+
import { createThemeCommandModule } from '../../theme/index.js';
|
|
20
|
+
import { createTerminalClientCommands } from '../terminal-client-commands.js';
|
|
21
|
+
|
|
22
|
+
import type { ITerminalClientCommand } from '../terminal-client-commands.js';
|
|
23
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
24
|
+
import type {
|
|
25
|
+
ICommandHostContext,
|
|
26
|
+
ICommandHostTerminalHandoff,
|
|
27
|
+
ICommandHostWorkspace,
|
|
28
|
+
ICommandModule,
|
|
29
|
+
IProviderCommandSettingsAdapter,
|
|
30
|
+
TProviderSettingsDocument,
|
|
31
|
+
} from '@robota-sdk/agent-framework';
|
|
32
|
+
import type {
|
|
33
|
+
IThemeCatalogueEntry,
|
|
34
|
+
IThemeCataloguePort,
|
|
35
|
+
} from '@robota-sdk/agent-interface-command';
|
|
36
|
+
|
|
37
|
+
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
38
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'agent-test-test-'));
|
|
39
|
+
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
const providerDefinitions: readonly IProviderDefinition[] = [];
|
|
42
|
+
const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
|
|
43
|
+
readMergedSettings: () => ({}) as TProviderSettingsDocument,
|
|
44
|
+
readTargetSettings: () => ({}) as TProviderSettingsDocument,
|
|
45
|
+
writeTargetSettings: () => undefined,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const THEMES: IThemeCatalogueEntry[] = [
|
|
49
|
+
{ id: 'dark', name: 'Dark', appearance: 'dark', source: 'built-in' },
|
|
50
|
+
{ id: 'light', name: 'Light', appearance: 'light', source: 'built-in' },
|
|
51
|
+
];
|
|
52
|
+
const themeCatalogue: IThemeCataloguePort = {
|
|
53
|
+
listThemes: () => THEMES,
|
|
54
|
+
getTheme: (id) => THEMES.find((theme) => theme.id === id),
|
|
55
|
+
getAppearance: () => ({
|
|
56
|
+
settings: { theme: 'dark', syntaxHighlighting: true, reducedMotion: false },
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function allPortsGiven(): readonly ITerminalClientCommand[] {
|
|
61
|
+
return createTerminalClientCommands({
|
|
62
|
+
keybindingsFile: { ensureFile: async () => '/tmp/keybindings.json' },
|
|
63
|
+
themeCatalogue,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function find(commands: readonly ITerminalClientCommand[], name: string): ITerminalClientCommand {
|
|
68
|
+
const command = commands.find((candidate) => candidate.name === name);
|
|
69
|
+
if (!command) throw new Error(`no terminal client command "${name}"`);
|
|
70
|
+
return command;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A terminal the command cannot take over: a host with no interactive TTY. */
|
|
74
|
+
function hostWithoutTerminal(): ICommandHostTerminalHandoff & ICommandHostWorkspace {
|
|
75
|
+
return {
|
|
76
|
+
canHandoffTerminal: () => false,
|
|
77
|
+
runWithTerminal: vi.fn(async () => {
|
|
78
|
+
throw new Error('a refused handoff must not be attempted');
|
|
79
|
+
}),
|
|
80
|
+
getCwd: () => '/tmp',
|
|
81
|
+
getCommandInvocationSource: () => 'user',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What the in-process command answers for the same host and arguments. */
|
|
86
|
+
async function inProcess(module: ICommandModule, host: unknown, args: string) {
|
|
87
|
+
const command = module.systemCommands?.[0];
|
|
88
|
+
if (!command) throw new Error(`module ${module.name} has no command`);
|
|
89
|
+
return command.execute(host as ICommandHostContext, args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('createTerminalClientCommands (#3189)', () => {
|
|
93
|
+
it('names exactly the commands the default assembly marks as client-run', () => {
|
|
94
|
+
const { modules } = createDefaultCommandModules({
|
|
95
|
+
cwd: '/tmp',
|
|
96
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
97
|
+
providerDefinitions,
|
|
98
|
+
providerSettingsAdapter,
|
|
99
|
+
});
|
|
100
|
+
const clientRun = modules
|
|
101
|
+
.flatMap((module) => module.systemCommands ?? [])
|
|
102
|
+
.filter((command) => command.runner === 'client')
|
|
103
|
+
.map((command) => command.name)
|
|
104
|
+
.sort();
|
|
105
|
+
|
|
106
|
+
expect(clientRun.length).toBeGreaterThan(0);
|
|
107
|
+
expect(
|
|
108
|
+
allPortsGiven()
|
|
109
|
+
.map((command) => command.name)
|
|
110
|
+
.sort(),
|
|
111
|
+
).toEqual(clientRun);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves out a client command whose module the selection disables, as the default assembly does', () => {
|
|
115
|
+
const selection = { disabledCommandModules: ['agent-command-shell'] };
|
|
116
|
+
const { modules } = createDefaultCommandModules({
|
|
117
|
+
cwd: '/tmp',
|
|
118
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
119
|
+
providerDefinitions,
|
|
120
|
+
providerSettingsAdapter,
|
|
121
|
+
...selection,
|
|
122
|
+
});
|
|
123
|
+
const clientRun = modules
|
|
124
|
+
.flatMap((module) => module.systemCommands ?? [])
|
|
125
|
+
.filter((command) => command.runner === 'client')
|
|
126
|
+
.map((command) => command.name)
|
|
127
|
+
.sort();
|
|
128
|
+
|
|
129
|
+
const names = createTerminalClientCommands({ themeCatalogue, ...selection })
|
|
130
|
+
.map((command) => command.name)
|
|
131
|
+
.sort();
|
|
132
|
+
|
|
133
|
+
expect(names).not.toContain('shell');
|
|
134
|
+
expect(names).toEqual(clientRun);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps only the enabled client modules, and a disabled name wins over an enabled one', () => {
|
|
138
|
+
const names = createTerminalClientCommands({
|
|
139
|
+
enabledCommandModules: ['agent-command-theme', 'agent-command-editor', 'agent-command-help'],
|
|
140
|
+
disabledCommandModules: ['agent-command-editor'],
|
|
141
|
+
}).map((command) => command.name);
|
|
142
|
+
|
|
143
|
+
expect(names).toEqual(['theme']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('with no selection, offers all four client commands', () => {
|
|
147
|
+
expect(
|
|
148
|
+
createTerminalClientCommands({})
|
|
149
|
+
.map((command) => command.name)
|
|
150
|
+
.sort(),
|
|
151
|
+
).toEqual(['editor', 'keybindings', 'shell', 'theme']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('/shell refuses a host that cannot hand off the terminal exactly as the in-process command does', async () => {
|
|
155
|
+
const host = hostWithoutTerminal();
|
|
156
|
+
|
|
157
|
+
const client = await find(allPortsGiven(), 'shell').execute(host, 'ls');
|
|
158
|
+
|
|
159
|
+
expect(client).toEqual(await inProcess(createShellCommandModule(), host, 'ls'));
|
|
160
|
+
expect(client.success).toBe(false);
|
|
161
|
+
expect(host.runWithTerminal).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('/editor and /keybindings refuse a host without a terminal as their in-process commands do', async () => {
|
|
165
|
+
const keybindingsFile = { ensureFile: vi.fn(async () => '/tmp/keybindings.json') };
|
|
166
|
+
const commands = createTerminalClientCommands({ keybindingsFile, themeCatalogue });
|
|
167
|
+
|
|
168
|
+
const editorHost = hostWithoutTerminal();
|
|
169
|
+
const editor = await find(commands, 'editor').execute(editorHost, 'draft');
|
|
170
|
+
expect(editor).toEqual(await inProcess(createEditorCommandModule(), editorHost, 'draft'));
|
|
171
|
+
expect(editor.success).toBe(false);
|
|
172
|
+
|
|
173
|
+
const keybindingsHost = hostWithoutTerminal();
|
|
174
|
+
const keybindings = await find(commands, 'keybindings').execute(keybindingsHost, '');
|
|
175
|
+
expect(keybindings).toEqual(
|
|
176
|
+
await inProcess(createKeybindingsCommandModule(keybindingsFile), keybindingsHost, ''),
|
|
177
|
+
);
|
|
178
|
+
expect(keybindings.success).toBe(false);
|
|
179
|
+
expect(keybindingsFile.ensureFile).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('/theme with no arguments asks the terminal to open its theme picker', async () => {
|
|
183
|
+
const result = await find(allPortsGiven(), 'theme').execute(hostWithoutTerminal(), '');
|
|
184
|
+
|
|
185
|
+
expect(result).toMatchObject({ success: true, uiIntents: [{ type: 'show-theme-picker' }] });
|
|
186
|
+
expect(result.hostActions).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('/theme with a known id returns one appearance settings patch for the terminal to write', async () => {
|
|
190
|
+
const result = await find(allPortsGiven(), 'theme').execute(hostWithoutTerminal(), 'light');
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
expect(result.hostActions).toEqual([
|
|
194
|
+
{ type: 'appearance-settings-patch', patch: { theme: 'light' } },
|
|
195
|
+
]);
|
|
196
|
+
expect(result).toEqual(await inProcess(createThemeCommandModule(themeCatalogue), {}, 'light'));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('without their ports, /keybindings and /theme still run on the terminal and answer as in-process', async () => {
|
|
200
|
+
const commands = createTerminalClientCommands({});
|
|
201
|
+
// The set does not shrink: a command missing from it would be sent to the daemon instead.
|
|
202
|
+
expect(commands.map((command) => command.name).sort()).toEqual(
|
|
203
|
+
allPortsGiven()
|
|
204
|
+
.map((command) => command.name)
|
|
205
|
+
.sort(),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const host = hostWithoutTerminal();
|
|
209
|
+
expect(await find(commands, 'keybindings').execute(host, '')).toEqual(
|
|
210
|
+
await inProcess(createKeybindingsCommandModule(undefined), host, ''),
|
|
211
|
+
);
|
|
212
|
+
expect(await find(commands, 'theme').execute(host, 'light')).toEqual(
|
|
213
|
+
await inProcess(createThemeCommandModule(undefined), host, 'light'),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3189: the commands that belong to the terminal the user sits at — the ones the default assembly
|
|
3
|
+
* marks `runner: 'client'`. A terminal attached to a workspace daemon runs these in its own process,
|
|
4
|
+
* on its own terminal and working directory, and sends nothing to the daemon.
|
|
5
|
+
*
|
|
6
|
+
* Each one is the in-process command's own execute function, so an attached terminal answers exactly
|
|
7
|
+
* as a standalone one does; only where it runs changes.
|
|
8
|
+
*/
|
|
9
|
+
import { selectCommandModules } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import { createEditorCommandModule } from '../editor/editor-command-module.js';
|
|
12
|
+
import { executeEditorCommand } from '../editor/editor-command.js';
|
|
13
|
+
import {
|
|
14
|
+
createKeybindingsCommandModule,
|
|
15
|
+
executeKeybindingsCommand,
|
|
16
|
+
} from '../keybindings/keybindings-command-module.js';
|
|
17
|
+
import { createShellCommandModule } from '../shell/shell-command-module.js';
|
|
18
|
+
import { executeShellCommand } from '../shell/shell-command.js';
|
|
19
|
+
import { createThemeCommandModule, executeThemeCommand } from '../theme/theme-command-module.js';
|
|
20
|
+
|
|
21
|
+
import type { IKeybindingsFilePort } from '../keybindings/keybindings-command-module.js';
|
|
22
|
+
import type {
|
|
23
|
+
ICommandHostTerminalHandoff,
|
|
24
|
+
ICommandHostWorkspace,
|
|
25
|
+
ICommandModule,
|
|
26
|
+
} from '@robota-sdk/agent-framework';
|
|
27
|
+
import type { ICommandResult, IThemeCataloguePort } from '@robota-sdk/agent-interface-command';
|
|
28
|
+
|
|
29
|
+
/** A command the terminal runs itself, with the terminal's own handoff and working directory. */
|
|
30
|
+
export interface ITerminalClientCommand {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
execute(
|
|
33
|
+
host: ICommandHostTerminalHandoff & ICommandHostWorkspace,
|
|
34
|
+
args: string,
|
|
35
|
+
): ICommandResult | Promise<ICommandResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The terminal's own capabilities — the same ones its in-process command modules are given. */
|
|
39
|
+
export interface ITerminalClientCommandOptions {
|
|
40
|
+
/** The shell `/shell` runs; absent → the platform shell, as for the in-process command. */
|
|
41
|
+
readonly shellExecutable?: string;
|
|
42
|
+
/** Product-owned prefix for `/editor`'s temporary directory; absent → the command's default. */
|
|
43
|
+
readonly editorTemporaryDirectoryPrefix?: string;
|
|
44
|
+
/** Absent → `/keybindings` answers where key bindings live, as the in-process command does. */
|
|
45
|
+
readonly keybindingsFile?: IKeybindingsFilePort;
|
|
46
|
+
/** Absent → `/theme` answers where themes live, as the in-process command does. */
|
|
47
|
+
readonly themeCatalogue?: IThemeCataloguePort;
|
|
48
|
+
/**
|
|
49
|
+
* The same module selection the default assembly is given (`agent-command-shell`, …): a module it
|
|
50
|
+
* leaves out is left out here too, so an attached terminal offers no command a standalone one
|
|
51
|
+
* would not. Absent → every client-run module.
|
|
52
|
+
*/
|
|
53
|
+
readonly enabledCommandModules?: readonly string[];
|
|
54
|
+
/** Applied after `enabledCommandModules`, as in the default assembly (deny > allow). */
|
|
55
|
+
readonly disabledCommandModules?: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type TClientExecute = ITerminalClientCommand['execute'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every client-run command the module selection keeps, whether or not its port is given. The set
|
|
62
|
+
* shrinks only with the selection, never with a missing port: the attached terminal routes by it, so
|
|
63
|
+
* a selected command left out would run on the daemon instead — and without its port the command
|
|
64
|
+
* still answers as its in-process twin does.
|
|
65
|
+
*/
|
|
66
|
+
export function createTerminalClientCommands(
|
|
67
|
+
options: ITerminalClientCommandOptions,
|
|
68
|
+
): readonly ITerminalClientCommand[] {
|
|
69
|
+
const {
|
|
70
|
+
shellExecutable,
|
|
71
|
+
editorTemporaryDirectoryPrefix,
|
|
72
|
+
keybindingsFile,
|
|
73
|
+
themeCatalogue,
|
|
74
|
+
enabledCommandModules,
|
|
75
|
+
disabledCommandModules,
|
|
76
|
+
} = options;
|
|
77
|
+
// Each client command is paired with its in-process module, so selection matches by the module's
|
|
78
|
+
// own name and the command keeps that module's command name.
|
|
79
|
+
const candidates: ReadonlyArray<readonly [ICommandModule, TClientExecute]> = [
|
|
80
|
+
[
|
|
81
|
+
createShellCommandModule(shellExecutable),
|
|
82
|
+
(host, args) => executeShellCommand(host, args, shellExecutable),
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
createEditorCommandModule(editorTemporaryDirectoryPrefix),
|
|
86
|
+
(host, args) => executeEditorCommand(host, args, editorTemporaryDirectoryPrefix),
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
createKeybindingsCommandModule(keybindingsFile),
|
|
90
|
+
(host) => executeKeybindingsCommand(keybindingsFile, host),
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
createThemeCommandModule(themeCatalogue),
|
|
94
|
+
(_host, args) => executeThemeCommand(themeCatalogue, args),
|
|
95
|
+
],
|
|
96
|
+
];
|
|
97
|
+
const selected = new Set(
|
|
98
|
+
selectCommandModules(
|
|
99
|
+
candidates.map(([module]) => module),
|
|
100
|
+
enabledCommandModules,
|
|
101
|
+
disabledCommandModules,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return candidates
|
|
105
|
+
.filter(([module]) => selected.has(module))
|
|
106
|
+
.flatMap(([module, execute]) =>
|
|
107
|
+
(module.systemCommands ?? []).map((command) => ({ name: command.name, execute })),
|
|
108
|
+
);
|
|
109
|
+
}
|