@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.
Files changed (31) hide show
  1. package/README.md +89 -0
  2. package/bin/entrypoints/cli/create-program.js +13 -0
  3. package/bin/entrypoints/cli/execute-command.js +10 -0
  4. package/bin/modules/interactive-guidance/index.js +1 -1
  5. package/bin/modules/interactive-guidance/prompts.js +34 -1
  6. package/bin/modules/mcp-server-selection-management/index.js +3 -0
  7. package/bin/modules/mcp-server-selection-management/list-mcp-servers/command.js +1 -0
  8. package/bin/modules/mcp-server-selection-management/list-mcp-servers/handler.js +46 -0
  9. package/bin/modules/mcp-server-selection-management/stop-mcp-server/command.js +1 -0
  10. package/bin/modules/mcp-server-selection-management/stop-mcp-server/handler.js +194 -0
  11. package/bin/modules/mcp-server-selection-management/use-mcp-server/command.js +1 -0
  12. package/bin/modules/mcp-server-selection-management/use-mcp-server/handler.js +127 -0
  13. package/bin/modules/project-adoption/handler.js +37 -11
  14. package/bin/modules/skill-selection-management/list-skills/handler.js +11 -1
  15. package/bin/modules/skill-selection-management/stop-managing-file/handler.js +7 -1
  16. package/bin/modules/skill-selection-management/use-skill/handler.js +35 -2
  17. package/bin/modules/sync-review/apply-action.js +5 -1
  18. package/bin/modules/sync-review/sync-preview.js +20 -6
  19. package/bin/modules/template-catalog-rendering/index.js +1 -1
  20. package/bin/modules/template-catalog-rendering/templates.js +59 -4
  21. package/bin/modules/workflow-health-diagnosis/handler.js +13 -3
  22. package/bin/modules/workflow-state-registry/state.js +4 -0
  23. package/bin/modules/workflow-target-planning/catalog.js +34 -0
  24. package/bin/modules/workflow-target-planning/index.js +2 -2
  25. package/bin/modules/workflow-target-planning/workflow-targets.js +82 -9
  26. package/package.json +1 -1
  27. package/templates/manifest.json +27 -6
  28. package/templates/mcp/claude/.mcp.json +3 -0
  29. package/templates/mcp/gemini/settings.json +3 -0
  30. package/templates/skills/postgresql-expert/SKILL.md +72 -0
  31. 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,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,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,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
- export async function handleInitProject(_command) {
11
- await renderIntro();
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 selectedLanguageSkills = await askForLanguageSkills();
32
- const selectedFrameworkSkills = await askForFrameworkSkills();
33
- const selectedArchitectureSkill = await askForArchitectureSkill();
34
- const selectedWorkflowSkills = await askForWorkflowSkills();
35
- const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills);
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({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, targets });
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
  }