@juancr11/sibu 0.7.0 → 0.9.0
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/README.md +89 -0
- package/bin/entrypoints/cli/create-program.js +13 -0
- package/bin/entrypoints/cli/execute-command.js +10 -0
- package/bin/modules/interactive-guidance/index.js +1 -1
- package/bin/modules/interactive-guidance/prompts.js +34 -1
- package/bin/modules/mcp-server-selection-management/index.js +3 -0
- package/bin/modules/mcp-server-selection-management/list-mcp-servers/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/list-mcp-servers/handler.js +46 -0
- package/bin/modules/mcp-server-selection-management/stop-mcp-server/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/stop-mcp-server/handler.js +194 -0
- package/bin/modules/mcp-server-selection-management/use-mcp-server/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/use-mcp-server/handler.js +127 -0
- package/bin/modules/project-adoption/handler.js +37 -11
- package/bin/modules/skill-selection-management/list-skills/handler.js +11 -1
- package/bin/modules/skill-selection-management/stop-managing-file/handler.js +7 -1
- package/bin/modules/skill-selection-management/use-skill/handler.js +35 -2
- package/bin/modules/sync-review/apply-action.js +5 -1
- package/bin/modules/sync-review/sync-preview.js +20 -6
- package/bin/modules/template-catalog-rendering/index.js +1 -1
- package/bin/modules/template-catalog-rendering/templates.js +59 -4
- package/bin/modules/workflow-health-diagnosis/handler.js +13 -3
- package/bin/modules/workflow-state-registry/state.js +4 -0
- package/bin/modules/workflow-target-planning/catalog.js +34 -0
- package/bin/modules/workflow-target-planning/index.js +2 -2
- package/bin/modules/workflow-target-planning/workflow-targets.js +82 -9
- package/package.json +1 -1
- package/templates/manifest.json +27 -6
- package/templates/mcp/claude/.mcp.json +3 -0
- package/templates/mcp/gemini/settings.json +3 -0
- package/templates/skills/postgresql-expert/SKILL.md +72 -0
- package/templates/skills/scrum-master-planner/SKILL.md +22 -0
package/README.md
CHANGED
|
@@ -42,6 +42,95 @@ Use `sibu skills list` to list available workflow skills and see which selectabl
|
|
|
42
42
|
|
|
43
43
|
Use `sibu skills stop <file>` to stop managing an Sibu-tracked workflow file. The file is marked as `unmanaged` in `.sibu/state.json`, removed from the selected skill state when applicable, and the CLI asks whether to keep or delete the local file.
|
|
44
44
|
|
|
45
|
+
## MCP server setup
|
|
46
|
+
|
|
47
|
+
Sibu can generate MCP server configuration for supported agents. The first supported server is GitHub's official MCP server.
|
|
48
|
+
|
|
49
|
+
Sibu only writes and tracks MCP config files. Runtime prerequisites, credentials, and provider authentication remain user-owned.
|
|
50
|
+
|
|
51
|
+
### 1. Confirm available MCP servers
|
|
52
|
+
|
|
53
|
+
From a Sibu-managed project, run:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
sibu mcp list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This shows each available MCP server and whether it is already selected for the project.
|
|
60
|
+
|
|
61
|
+
### 2. Configure the GitHub MCP server
|
|
62
|
+
|
|
63
|
+
You can select GitHub during `sibu init`, or add it later:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
sibu mcp use github
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Sibu writes the relevant config for the agents selected in the project:
|
|
70
|
+
|
|
71
|
+
- Codex: `.codex/config.toml`
|
|
72
|
+
- Claude: `.mcp.json`
|
|
73
|
+
- Gemini: `.gemini/settings.json`
|
|
74
|
+
|
|
75
|
+
All supported agents use GitHub's hosted MCP endpoint instead of requiring Docker. Codex uses Codex's native bearer-token environment variable setting:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[mcp_servers.github]
|
|
79
|
+
url = "https://api.githubcopilot.com/mcp/"
|
|
80
|
+
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Claude uses HTTP MCP config with an authorization header:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"github": {
|
|
89
|
+
"type": "http",
|
|
90
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
91
|
+
"headers": {
|
|
92
|
+
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Gemini uses streamable HTTP config with an authorization header:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"github": {
|
|
105
|
+
"httpUrl": "https://api.githubcopilot.com/mcp/",
|
|
106
|
+
"headers": {
|
|
107
|
+
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Provide GitHub credentials
|
|
115
|
+
|
|
116
|
+
Create a GitHub personal access token with the repository permissions your agent should have, then expose it as an environment variable before launching the agent:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not commit tokens or write them into Sibu-managed config files. The generated configs read `GITHUB_PERSONAL_ACCESS_TOKEN` from the environment so credentials can stay outside the repository.
|
|
123
|
+
|
|
124
|
+
### 4. Stop managing the GitHub MCP server
|
|
125
|
+
|
|
126
|
+
To remove GitHub from Sibu's selected MCP server state, run:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
sibu mcp stop github
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Sibu updates generated agent config where possible and asks whether to keep or delete MCP-only config files.
|
|
133
|
+
|
|
45
134
|
## Release notes and changelog
|
|
46
135
|
|
|
47
136
|
For every Sibu release, update both release-note locations:
|
|
@@ -29,5 +29,18 @@ export function createProgram() {
|
|
|
29
29
|
.command('stop <skill_name>')
|
|
30
30
|
.description('Stop managing one selected Sibu skill')
|
|
31
31
|
.action((skillName) => executeCliCommand({ type: 'skills:stop', skillName }));
|
|
32
|
+
const mcp = cli.command('mcp').description('Manage Sibu MCP server configuration');
|
|
33
|
+
mcp
|
|
34
|
+
.command('list')
|
|
35
|
+
.description('List available MCP servers')
|
|
36
|
+
.action(() => executeCliCommand({ type: 'mcp:list' }));
|
|
37
|
+
mcp
|
|
38
|
+
.command('use <mcp_server_id>')
|
|
39
|
+
.description('Add one available MCP server to a clean Sibu workflow')
|
|
40
|
+
.action((serverId) => executeCliCommand({ type: 'mcp:use', serverId }));
|
|
41
|
+
mcp
|
|
42
|
+
.command('stop <mcp_server_id>')
|
|
43
|
+
.description('Stop managing one selected MCP server')
|
|
44
|
+
.action((serverId) => executeCliCommand({ type: 'mcp:stop', serverId }));
|
|
32
45
|
return cli;
|
|
33
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { handleDoctorProject } from '../../modules/workflow-health-diagnosis/index.js';
|
|
2
2
|
import { handleInitProject } from '../../modules/project-adoption/index.js';
|
|
3
|
+
import { handleListMcpServers, handleStopMcpServer, handleUseMcpServer } from '../../modules/mcp-server-selection-management/index.js';
|
|
3
4
|
import { handleListSkills } from '../../modules/skill-selection-management/index.js';
|
|
4
5
|
import { handleStopManagingFile } from '../../modules/skill-selection-management/index.js';
|
|
5
6
|
import { handleSyncProject } from '../../modules/sync-review/index.js';
|
|
@@ -24,5 +25,14 @@ export async function executeCliCommand(command) {
|
|
|
24
25
|
case 'skills:use':
|
|
25
26
|
await handleUseSkill(command);
|
|
26
27
|
return;
|
|
28
|
+
case 'mcp:list':
|
|
29
|
+
await handleListMcpServers(command);
|
|
30
|
+
return;
|
|
31
|
+
case 'mcp:use':
|
|
32
|
+
await handleUseMcpServer(command);
|
|
33
|
+
return;
|
|
34
|
+
case 'mcp:stop':
|
|
35
|
+
await handleStopMcpServer(command);
|
|
36
|
+
return;
|
|
27
37
|
}
|
|
28
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { askForArchitectureSkill, askForFrameworkSkills, askForLanguageSkills, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
|
|
1
|
+
export { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, MCP_SERVER_SELECTION_MESSAGE, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
|
|
@@ -3,8 +3,9 @@ import { cancel, isCancel, multiselect, select, text } from '@clack/prompts';
|
|
|
3
3
|
import gradient from 'gradient-string';
|
|
4
4
|
import { Box, Text, render, useApp } from 'ink';
|
|
5
5
|
import { useEffect } from 'react';
|
|
6
|
-
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS } from '../workflow-target-planning/index.js';
|
|
6
|
+
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_MCP_SERVERS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, } from '../workflow-target-planning/index.js';
|
|
7
7
|
const NONE_OPTION_ID = 'none';
|
|
8
|
+
export const MCP_SERVER_SELECTION_MESSAGE = 'Select optional MCP servers to configure. Sibu writes config files only; you own prerequisites and authentication.';
|
|
8
9
|
export async function renderIntro() {
|
|
9
10
|
console.log(gradient(['#39ff14', '#00e5ff', '#9b5de5']).multiline('⧖ S I B U ⧖'));
|
|
10
11
|
const app = render(_jsx(IntroPanel, {}));
|
|
@@ -56,6 +57,38 @@ export async function askForFrameworkSkills() {
|
|
|
56
57
|
cancelMessage: 'Initialization cancelled.',
|
|
57
58
|
});
|
|
58
59
|
}
|
|
60
|
+
export async function askForDatabaseSkills() {
|
|
61
|
+
const selectedDatabaseSkillIds = await multiselect({
|
|
62
|
+
message: 'Select the database skills this project should support.',
|
|
63
|
+
required: false,
|
|
64
|
+
options: SELECTABLE_DATABASE_SKILLS.map((skill) => ({
|
|
65
|
+
value: skill.id,
|
|
66
|
+
label: skill.name,
|
|
67
|
+
hint: skill.description,
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
if (isCancel(selectedDatabaseSkillIds)) {
|
|
71
|
+
cancel('Initialization cancelled.');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
return SELECTABLE_DATABASE_SKILLS.filter((skill) => selectedDatabaseSkillIds.includes(skill.id));
|
|
75
|
+
}
|
|
76
|
+
export async function askForMcpServers() {
|
|
77
|
+
const selectedMcpServerIds = await multiselect({
|
|
78
|
+
message: MCP_SERVER_SELECTION_MESSAGE,
|
|
79
|
+
required: false,
|
|
80
|
+
options: SELECTABLE_MCP_SERVERS.map((server) => ({
|
|
81
|
+
value: server.id,
|
|
82
|
+
label: server.name,
|
|
83
|
+
hint: server.description,
|
|
84
|
+
})),
|
|
85
|
+
});
|
|
86
|
+
if (isCancel(selectedMcpServerIds)) {
|
|
87
|
+
cancel('Initialization cancelled.');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
return SELECTABLE_MCP_SERVERS.filter((server) => selectedMcpServerIds.includes(server.id));
|
|
91
|
+
}
|
|
59
92
|
async function askForFrameworkSkillSelection({ message, cancelMessage, }) {
|
|
60
93
|
const selectedFrameworkSkillIds = await multiselect({
|
|
61
94
|
message,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { getMcpServerListItems, handleListMcpServers } from './list-mcp-servers/handler.js';
|
|
2
|
+
export { getNextMcpSelection, handleUseMcpServer } from './use-mcp-server/handler.js';
|
|
3
|
+
export { applyStoppedMcpFileDeleteDecision, getNextStoppedMcpSelection, handleStopMcpServer, stopSelectedMcpServer } from './stop-mcp-server/handler.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { intro, log, outro } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { SELECTABLE_MCP_SERVERS } from '../../workflow-target-planning/index.js';
|
|
4
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
5
|
+
import { renderIntro } from '../../interactive-guidance/index.js';
|
|
6
|
+
import { readStateForDoctor } from '../../workflow-state-registry/index.js';
|
|
7
|
+
const defaultDependencies = {
|
|
8
|
+
renderIntro,
|
|
9
|
+
intro,
|
|
10
|
+
logMessage: log.message,
|
|
11
|
+
logWarn: log.warn,
|
|
12
|
+
logInfo: log.info,
|
|
13
|
+
outro,
|
|
14
|
+
getStatePath: () => getProjectContext().statePath,
|
|
15
|
+
readState: readStateForDoctor,
|
|
16
|
+
};
|
|
17
|
+
export async function handleListMcpServers(_command, dependencies = defaultDependencies) {
|
|
18
|
+
await dependencies.renderIntro();
|
|
19
|
+
dependencies.intro(chalk.cyan('Available MCP servers'));
|
|
20
|
+
const statePath = dependencies.getStatePath();
|
|
21
|
+
const stateResult = dependencies.readState(statePath);
|
|
22
|
+
const state = stateResult.ok ? stateResult.state : undefined;
|
|
23
|
+
if (!stateResult.ok) {
|
|
24
|
+
dependencies.logWarn(`${stateResult.message} Showing available MCP servers without project state.`);
|
|
25
|
+
dependencies.logInfo('Run `sibu init` before selecting project MCP servers.');
|
|
26
|
+
}
|
|
27
|
+
dependencies.logInfo('Sibu configures MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
|
|
28
|
+
dependencies.logMessage(chalk.bold('MCP servers'));
|
|
29
|
+
for (const server of getMcpServerListItems(state)) {
|
|
30
|
+
const marker = server.selected ? chalk.green('● selected') : chalk.dim('○ available');
|
|
31
|
+
console.log(` ${marker} ${chalk.bold(server.name)} ${chalk.dim(`(${server.id})`)}`);
|
|
32
|
+
console.log(` ${chalk.dim(server.description)}`);
|
|
33
|
+
console.log(` ${chalk.dim(`Source: ${server.source}`)}`);
|
|
34
|
+
}
|
|
35
|
+
dependencies.outro(chalk.green('MCP server list ready.'));
|
|
36
|
+
}
|
|
37
|
+
export function getMcpServerListItems(state) {
|
|
38
|
+
const selectedServerIds = new Set(state?.selectedMcpServers ?? []);
|
|
39
|
+
return SELECTABLE_MCP_SERVERS.map((server) => ({
|
|
40
|
+
name: server.name,
|
|
41
|
+
id: server.id,
|
|
42
|
+
description: server.description,
|
|
43
|
+
source: server.source,
|
|
44
|
+
selected: selectedServerIds.has(server.id),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cancel, isCancel, log, select } from '@clack/prompts';
|
|
4
|
+
import { STATE_RELATIVE_PATH } from '../../../shared/catalog.js';
|
|
5
|
+
import { readFileHashIfPresent, sha256 } from '../../../shared/hash.js';
|
|
6
|
+
import { removeUndefinedFields } from '../../../shared/object.js';
|
|
7
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
8
|
+
import { renderMissingWorkflowFiles, resolveSelectableMcpServerById } from '../../workflow-target-planning/index.js';
|
|
9
|
+
import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
|
|
10
|
+
import { getTemplateVersion, readTemplateManifest } from '../../template-catalog-rendering/index.js';
|
|
11
|
+
import { cloneState, writeStateFile } from '../../workflow-state-registry/index.js';
|
|
12
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../../workflow-target-planning/index.js';
|
|
13
|
+
export async function handleStopMcpServer(command) {
|
|
14
|
+
const { rootPath, statePath } = getProjectContext();
|
|
15
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
16
|
+
if (!readiness.ok) {
|
|
17
|
+
log.error(readiness.message);
|
|
18
|
+
log.info(readiness.hint);
|
|
19
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
20
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
21
|
+
}
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const result = stopSelectedMcpServer({ rootPath, state: readiness.state, serverId: command.serverId });
|
|
26
|
+
switch (result.status) {
|
|
27
|
+
case 'blocked':
|
|
28
|
+
log.error(result.message);
|
|
29
|
+
if (result.hint) {
|
|
30
|
+
log.info(result.hint);
|
|
31
|
+
}
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
case 'noop':
|
|
35
|
+
log.success(result.message);
|
|
36
|
+
log.info('No files changed.');
|
|
37
|
+
return;
|
|
38
|
+
case 'stopped':
|
|
39
|
+
writeStateFile(statePath, result.state);
|
|
40
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
41
|
+
log.success(`Stopped ${result.serverName}.`);
|
|
42
|
+
for (const [index, stoppedPath] of result.stoppedPaths.entries()) {
|
|
43
|
+
await askToDeleteStoppedMcpFile(stoppedPath, result.stoppedFiles[index]);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getNextStoppedMcpSelection(state, serverId) {
|
|
49
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
50
|
+
if (!resolution.ok) {
|
|
51
|
+
return { status: 'blocked', message: resolution.message };
|
|
52
|
+
}
|
|
53
|
+
const selectedMcpServerIds = [...(state.selectedMcpServers ?? [])];
|
|
54
|
+
if (!selectedMcpServerIds.includes(resolution.resolved.server.id)) {
|
|
55
|
+
return { status: 'noop', message: `${resolution.resolved.server.name} is not selected.` };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
status: 'selected',
|
|
59
|
+
serverName: resolution.resolved.server.name,
|
|
60
|
+
remainingMcpServers: selectedMcpServerIds.filter((selectedServerId) => selectedServerId !== resolution.resolved.server.id).map(getMcpServerById),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function stopSelectedMcpServer({ rootPath, state, serverId }) {
|
|
64
|
+
const selectionResult = getNextStoppedMcpSelection(state, serverId);
|
|
65
|
+
if (selectionResult.status !== 'selected') {
|
|
66
|
+
return selectionResult;
|
|
67
|
+
}
|
|
68
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
69
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
70
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
71
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
72
|
+
const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
73
|
+
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
74
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
|
|
75
|
+
const nextTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.remainingMcpServers);
|
|
76
|
+
const manifest = readTemplateManifest();
|
|
77
|
+
const nextState = cloneState(state);
|
|
78
|
+
const nextTargetsByPath = new Map(nextTargets.map((target) => [target.targetPath, target]));
|
|
79
|
+
const stoppedPaths = [];
|
|
80
|
+
const stoppedFiles = [];
|
|
81
|
+
nextState.selectedMcpServers = selectionResult.remainingMcpServers.map((server) => server.id);
|
|
82
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
83
|
+
nextState.updatedAt = new Date().toISOString();
|
|
84
|
+
for (const previousTarget of previousTargets.filter((target) => target.mcpConfigAgentId)) {
|
|
85
|
+
const relativePath = path.relative(rootPath, previousTarget.targetPath);
|
|
86
|
+
const managedFile = nextState.managedFiles[relativePath];
|
|
87
|
+
if (!managedFile) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'blocked',
|
|
90
|
+
message: `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
|
|
91
|
+
hint: 'Run `sibu sync` to review workflow state before stopping this MCP server.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const nextTarget = nextTargetsByPath.get(previousTarget.targetPath);
|
|
95
|
+
if (nextTarget) {
|
|
96
|
+
const [file] = renderMissingWorkflowFiles({
|
|
97
|
+
missingTargets: [nextTarget],
|
|
98
|
+
selectedLanguageSkills,
|
|
99
|
+
selectedFrameworkSkills,
|
|
100
|
+
selectedArchitectureSkill,
|
|
101
|
+
selectedWorkflowSkills,
|
|
102
|
+
selectedDatabaseSkills,
|
|
103
|
+
selectedMcpServers: selectionResult.remainingMcpServers,
|
|
104
|
+
});
|
|
105
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
106
|
+
fs.writeFileSync(file.targetPath, file.contents, 'utf8');
|
|
107
|
+
nextState.managedFiles[relativePath] = removeUndefinedFields({
|
|
108
|
+
...managedFile,
|
|
109
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
110
|
+
sha256: sha256(file.contents),
|
|
111
|
+
status: managedFile.status ?? 'managed',
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const stoppedFile = removeUndefinedFields({
|
|
116
|
+
...managedFile,
|
|
117
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
118
|
+
sha256: readFileHashIfPresent(previousTarget.targetPath) ?? managedFile.sha256,
|
|
119
|
+
status: 'unmanaged',
|
|
120
|
+
reason: 'Stopped by `sibu mcp stop`.',
|
|
121
|
+
});
|
|
122
|
+
nextState.managedFiles[relativePath] = stoppedFile;
|
|
123
|
+
stoppedPaths.push({ relativePath, absolutePath: previousTarget.targetPath });
|
|
124
|
+
stoppedFiles.push(stoppedFile);
|
|
125
|
+
}
|
|
126
|
+
return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, serverName: selectionResult.serverName };
|
|
127
|
+
}
|
|
128
|
+
export async function askToDeleteStoppedMcpFile(managedPath, managedFile) {
|
|
129
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
130
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
134
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const deleteAction = await select({
|
|
138
|
+
message: `Do you also want to delete ${managedPath.relativePath} for good?`,
|
|
139
|
+
options: [
|
|
140
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local MCP config file unchanged.' },
|
|
141
|
+
{ value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
if (isCancel(deleteAction)) {
|
|
145
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
const confirmDelete = deleteAction === 'delete' && hasLocalEdits(managedPath.absolutePath, managedFile) ? await askToConfirmEditedDelete(managedPath) : undefined;
|
|
149
|
+
applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete });
|
|
150
|
+
}
|
|
151
|
+
export function applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete, }) {
|
|
152
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
153
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
157
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (deleteAction === 'keep') {
|
|
161
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (hasLocalEdits(managedPath.absolutePath, managedFile) && confirmDelete !== 'delete') {
|
|
165
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
fs.unlinkSync(managedPath.absolutePath);
|
|
169
|
+
log.success(`Deleted ${managedPath.relativePath}.`);
|
|
170
|
+
}
|
|
171
|
+
async function askToConfirmEditedDelete(managedPath) {
|
|
172
|
+
const confirmDelete = await select({
|
|
173
|
+
message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
|
|
176
|
+
{ value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
if (isCancel(confirmDelete)) {
|
|
180
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
return confirmDelete;
|
|
184
|
+
}
|
|
185
|
+
function hasLocalEdits(filePath, managedFile) {
|
|
186
|
+
return readFileHashIfPresent(filePath) !== managedFile.sha256;
|
|
187
|
+
}
|
|
188
|
+
function getMcpServerById(serverId) {
|
|
189
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
190
|
+
if (!resolution.ok) {
|
|
191
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
192
|
+
}
|
|
193
|
+
return resolution.resolved.server;
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log } from '@clack/prompts';
|
|
4
|
+
import { STATE_RELATIVE_PATH } from '../../../shared/catalog.js';
|
|
5
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
6
|
+
import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
|
|
7
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, renderMissingWorkflowFiles, resolveSelectableMcpServerById, writeSibuState, } from '../../workflow-target-planning/index.js';
|
|
8
|
+
export async function handleUseMcpServer(command) {
|
|
9
|
+
const { rootPath, statePath } = getProjectContext();
|
|
10
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
11
|
+
if (!readiness.ok) {
|
|
12
|
+
log.error(readiness.message);
|
|
13
|
+
log.info(readiness.hint);
|
|
14
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
15
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
16
|
+
}
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const selectionResult = getNextMcpSelection(readiness.state, command.serverId);
|
|
21
|
+
switch (selectionResult.status) {
|
|
22
|
+
case 'noop':
|
|
23
|
+
log.success(selectionResult.message);
|
|
24
|
+
log.info('No files changed.');
|
|
25
|
+
return;
|
|
26
|
+
case 'blocked':
|
|
27
|
+
log.error(selectionResult.message);
|
|
28
|
+
if (selectionResult.hint) {
|
|
29
|
+
log.info(selectionResult.hint);
|
|
30
|
+
}
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
case 'selected':
|
|
34
|
+
applySelectedMcpServer({ rootPath, statePath, state: readiness.state, selectionResult });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function getNextMcpSelection(state, serverId) {
|
|
39
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
40
|
+
if (!resolution.ok) {
|
|
41
|
+
return { status: 'blocked', message: resolution.message };
|
|
42
|
+
}
|
|
43
|
+
const selectedMcpServerIds = [...(state.selectedMcpServers ?? [])];
|
|
44
|
+
if (selectedMcpServerIds.includes(resolution.resolved.server.id)) {
|
|
45
|
+
return { status: 'noop', message: `${resolution.resolved.server.name} is already selected.` };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
status: 'selected',
|
|
49
|
+
serverName: resolution.resolved.server.name,
|
|
50
|
+
selectedMcpServers: [...selectedMcpServerIds, resolution.resolved.server.id].map(getMcpServerById),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, }) {
|
|
54
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
55
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
56
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
57
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
58
|
+
const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
59
|
+
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
60
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
|
|
61
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.selectedMcpServers);
|
|
62
|
+
const affectedTargets = getAffectedMcpTargets(previousTargets, targets);
|
|
63
|
+
const preflightError = getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets });
|
|
64
|
+
if (preflightError) {
|
|
65
|
+
log.error(preflightError);
|
|
66
|
+
log.info('Run `sibu sync` to review workflow state before selecting an MCP server.');
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const files = renderMissingWorkflowFiles({
|
|
71
|
+
missingTargets: affectedTargets,
|
|
72
|
+
selectedLanguageSkills,
|
|
73
|
+
selectedFrameworkSkills,
|
|
74
|
+
selectedArchitectureSkill,
|
|
75
|
+
selectedWorkflowSkills,
|
|
76
|
+
selectedDatabaseSkills,
|
|
77
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
78
|
+
});
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const fileAlreadyExists = fs.existsSync(file.targetPath);
|
|
81
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
82
|
+
fs.writeFileSync(file.targetPath, file.contents, 'utf8');
|
|
83
|
+
log.success(`${fileAlreadyExists ? 'Updated' : 'Created'} ${file.label}`);
|
|
84
|
+
}
|
|
85
|
+
writeSibuState({
|
|
86
|
+
rootPath,
|
|
87
|
+
statePath,
|
|
88
|
+
selectedAgents,
|
|
89
|
+
selectedLanguageSkills,
|
|
90
|
+
selectedFrameworkSkills,
|
|
91
|
+
selectedArchitectureSkill,
|
|
92
|
+
selectedWorkflowSkills,
|
|
93
|
+
selectedDatabaseSkills,
|
|
94
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
95
|
+
targets,
|
|
96
|
+
});
|
|
97
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
98
|
+
log.success(`Added ${selectionResult.serverName}.`);
|
|
99
|
+
log.info('Sibu configured MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
|
|
100
|
+
}
|
|
101
|
+
function getAffectedMcpTargets(previousTargets, targets) {
|
|
102
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
103
|
+
return targets.filter((target) => target.mcpConfigAgentId || (!previousTargetPaths.has(target.targetPath) && target.targetKind === 'mcp-config'));
|
|
104
|
+
}
|
|
105
|
+
function getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets, }) {
|
|
106
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
107
|
+
for (const target of affectedTargets) {
|
|
108
|
+
const relativePath = path.relative(rootPath, target.targetPath);
|
|
109
|
+
if (previousTargetPaths.has(target.targetPath)) {
|
|
110
|
+
if (!state.managedFiles[relativePath]) {
|
|
111
|
+
return `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (fs.existsSync(target.targetPath)) {
|
|
116
|
+
return `${relativePath} already exists but is not recorded for this MCP selection.`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function getMcpServerById(serverId) {
|
|
122
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
123
|
+
if (!resolution.ok) {
|
|
124
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
125
|
+
}
|
|
126
|
+
return resolution.resolved.server;
|
|
127
|
+
}
|
|
@@ -4,11 +4,22 @@ import { intro, log, outro } from '@clack/prompts';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
6
6
|
import { getProjectContext } from '../../shared/paths.js';
|
|
7
|
-
import { askForArchitectureSkill, askForFrameworkSkills, askForLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro } from '../interactive-guidance/index.js';
|
|
7
|
+
import { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro, } from '../interactive-guidance/index.js';
|
|
8
8
|
import { readStateForDoctor } from '../workflow-state-registry/index.js';
|
|
9
9
|
import { getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../workflow-target-planning/index.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const defaultDependencies = {
|
|
11
|
+
renderIntro,
|
|
12
|
+
askForSupportedAgents,
|
|
13
|
+
askForMcpServers,
|
|
14
|
+
askForLanguageSkills,
|
|
15
|
+
askForFrameworkSkills,
|
|
16
|
+
askForDatabaseSkills,
|
|
17
|
+
askForArchitectureSkill,
|
|
18
|
+
askForWorkflowSkills,
|
|
19
|
+
askForProjectOverview,
|
|
20
|
+
};
|
|
21
|
+
export async function handleInitProject(_command, dependencies = defaultDependencies) {
|
|
22
|
+
await dependencies.renderIntro();
|
|
12
23
|
intro(chalk.cyan('Setting up Sibu'));
|
|
13
24
|
const { rootPath, statePath } = getProjectContext();
|
|
14
25
|
if (fs.existsSync(statePath)) {
|
|
@@ -27,12 +38,14 @@ export async function handleInitProject(_command) {
|
|
|
27
38
|
process.exitCode = 1;
|
|
28
39
|
return;
|
|
29
40
|
}
|
|
30
|
-
const selectedAgents = await askForSupportedAgents();
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
41
|
+
const selectedAgents = await dependencies.askForSupportedAgents();
|
|
42
|
+
const selectedMcpServers = await dependencies.askForMcpServers();
|
|
43
|
+
const selectedLanguageSkills = await dependencies.askForLanguageSkills();
|
|
44
|
+
const selectedFrameworkSkills = await dependencies.askForFrameworkSkills();
|
|
45
|
+
const selectedDatabaseSkills = await dependencies.askForDatabaseSkills();
|
|
46
|
+
const selectedArchitectureSkill = await dependencies.askForArchitectureSkill();
|
|
47
|
+
const selectedWorkflowSkills = await dependencies.askForWorkflowSkills();
|
|
48
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
|
|
36
49
|
const missingTargets = targets.filter((target) => !fs.existsSync(target.targetPath));
|
|
37
50
|
log.message('I will create this project’s initial AI workflow files.');
|
|
38
51
|
for (const target of targets) {
|
|
@@ -45,7 +58,7 @@ export async function handleInitProject(_command) {
|
|
|
45
58
|
}
|
|
46
59
|
log.info(`${STATE_RELATIVE_PATH} is missing. I will create it.`);
|
|
47
60
|
const shouldAskForOverview = missingTargets.some((target) => target.requiresProjectOverview);
|
|
48
|
-
const overview = shouldAskForOverview ? await askForProjectOverview() : undefined;
|
|
61
|
+
const overview = shouldAskForOverview ? await dependencies.askForProjectOverview() : undefined;
|
|
49
62
|
const files = renderMissingWorkflowFiles({
|
|
50
63
|
missingTargets,
|
|
51
64
|
overview,
|
|
@@ -53,13 +66,26 @@ export async function handleInitProject(_command) {
|
|
|
53
66
|
selectedFrameworkSkills,
|
|
54
67
|
selectedArchitectureSkill,
|
|
55
68
|
selectedWorkflowSkills,
|
|
69
|
+
selectedDatabaseSkills,
|
|
70
|
+
selectedMcpServers,
|
|
56
71
|
});
|
|
57
72
|
for (const file of files) {
|
|
58
73
|
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
59
74
|
fs.writeFileSync(file.targetPath, file.contents, { encoding: 'utf8', flag: 'wx' });
|
|
60
75
|
log.success(`Created ${file.label}`);
|
|
61
76
|
}
|
|
62
|
-
writeSibuState({
|
|
77
|
+
writeSibuState({
|
|
78
|
+
rootPath,
|
|
79
|
+
statePath,
|
|
80
|
+
selectedAgents,
|
|
81
|
+
selectedLanguageSkills,
|
|
82
|
+
selectedFrameworkSkills,
|
|
83
|
+
selectedArchitectureSkill,
|
|
84
|
+
selectedWorkflowSkills,
|
|
85
|
+
selectedDatabaseSkills,
|
|
86
|
+
selectedMcpServers,
|
|
87
|
+
targets,
|
|
88
|
+
});
|
|
63
89
|
log.success(`Created ${STATE_RELATIVE_PATH}`);
|
|
64
90
|
outro(chalk.green('Setup complete.'));
|
|
65
91
|
}
|