@juancr11/sibu 0.17.0 → 0.17.1

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 (58) hide show
  1. package/README.md +66 -29
  2. package/bin/entrypoints/cli/create-program.js +1 -1
  3. package/bin/entrypoints/cli/execute-command.js +4 -7
  4. package/bin/modules/agent-tool-configuration/index.js +1 -0
  5. package/bin/modules/agent-tool-configuration/mcp-config.js +134 -0
  6. package/bin/modules/interactive-guidance/prompts.js +1 -1
  7. package/bin/modules/mcp-server-selection-management/list-mcp-servers/handler.js +2 -2
  8. package/bin/modules/mcp-server-selection-management/stop-mcp-server/handler.js +5 -5
  9. package/bin/modules/mcp-server-selection-management/use-mcp-server/handler.js +4 -3
  10. package/bin/modules/skill-selection-management/list-skills/handler.js +2 -2
  11. package/bin/modules/skill-selection-management/stop-managing-file/handler.js +5 -4
  12. package/bin/modules/skill-selection-management/use-skill/handler.js +5 -4
  13. package/bin/modules/{sync-review → sync-review-orchestrator}/apply-action.js +4 -3
  14. package/bin/modules/{sync-review → sync-review-orchestrator}/handler.js +4 -4
  15. package/bin/modules/{sync-review → sync-review-orchestrator}/index.js +1 -0
  16. package/bin/modules/{sync-review → sync-review-orchestrator}/log-preview.js +1 -1
  17. package/bin/modules/{sync-review → sync-review-orchestrator}/sync-preview.js +3 -3
  18. package/bin/modules/{sync-review → sync-review-orchestrator}/unsupported-agent-cleanup.js +2 -2
  19. package/bin/modules/{workflow-mutation-readiness → sync-review-orchestrator}/workflow-mutation-readiness.js +4 -4
  20. package/bin/modules/{workflow-target-planning → template-catalog}/index.js +2 -1
  21. package/bin/modules/{template-catalog-rendering → template-catalog}/templates.js +42 -115
  22. package/bin/modules/workflow-configuration-manager/index.js +6 -0
  23. package/bin/modules/workflow-configuration-manager/list-mcp-servers/handler.js +46 -0
  24. package/bin/modules/workflow-configuration-manager/list-skills/command.js +1 -0
  25. package/bin/modules/workflow-configuration-manager/list-skills/handler.js +75 -0
  26. package/bin/modules/workflow-configuration-manager/stop-managing-file/command.js +1 -0
  27. package/bin/modules/workflow-configuration-manager/stop-managing-file/handler.js +221 -0
  28. package/bin/modules/workflow-configuration-manager/stop-mcp-server/command.js +1 -0
  29. package/bin/modules/workflow-configuration-manager/stop-mcp-server/handler.js +198 -0
  30. package/bin/modules/workflow-configuration-manager/use-mcp-server/command.js +1 -0
  31. package/bin/modules/workflow-configuration-manager/use-mcp-server/handler.js +193 -0
  32. package/bin/modules/workflow-configuration-manager/use-skill/command.js +1 -0
  33. package/bin/modules/workflow-configuration-manager/use-skill/handler.js +314 -0
  34. package/bin/modules/workflow-health-inspector/command.js +1 -0
  35. package/bin/modules/{workflow-health-diagnosis → workflow-health-inspector}/handler.js +7 -7
  36. package/bin/modules/workflow-installer/command.js +1 -0
  37. package/bin/modules/{project-adoption → workflow-installer}/handler.js +5 -4
  38. package/bin/modules/workflow-state-ledger/index.js +2 -0
  39. package/bin/modules/workflow-state-ledger/state-path.js +1 -0
  40. package/bin/modules/{workflow-state-registry → workflow-state-ledger}/state.js +40 -1
  41. package/bin/{modules/workflow-target-planning/workflow-targets.js → shared/expected-workflow-targets.js} +3 -104
  42. package/bin/shared/paths.js +1 -1
  43. package/bin/support/expected-workflow-targets.js +131 -0
  44. package/bin/support/interactive-guidance/index.js +1 -0
  45. package/bin/support/interactive-guidance/prompts.js +275 -0
  46. package/bin/support/version-advisory/index.js +1 -0
  47. package/bin/support/version-advisory/npm-version.js +205 -0
  48. package/package.json +2 -1
  49. package/bin/modules/template-catalog-rendering/index.js +0 -1
  50. package/bin/modules/workflow-mutation-readiness/index.js +0 -1
  51. package/bin/modules/workflow-state-registry/index.js +0 -1
  52. /package/bin/{modules/project-adoption/command.js → entrypoints/cli-command-surface/index.js} +0 -0
  53. /package/bin/modules/{sync-review → sync-review-orchestrator}/action-prompt.js +0 -0
  54. /package/bin/modules/{sync-review → sync-review-orchestrator}/command.js +0 -0
  55. /package/bin/modules/{workflow-target-planning → template-catalog}/catalog.js +0 -0
  56. /package/bin/modules/{workflow-health-diagnosis → workflow-configuration-manager/list-mcp-servers}/command.js +0 -0
  57. /package/bin/modules/{workflow-health-diagnosis → workflow-health-inspector}/index.js +0 -0
  58. /package/bin/modules/{project-adoption → workflow-installer}/index.js +0 -0
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getTemplatesPath } from '../../shared/paths.js';
4
+ import { renderMcpConfig, resolveMcpConfigAgentId } from '../agent-tool-configuration/index.js';
4
5
  export function readTemplate(relativePath) {
5
6
  return fs.readFileSync(path.join(getTemplatesPath(), relativePath), 'utf8');
6
7
  }
@@ -18,9 +19,24 @@ export function getTemplateVersion(manifest, templateRelativePath) {
18
19
  }
19
20
  return template.version;
20
21
  }
22
+ function isTemplateManifest(value) {
23
+ if (!value || typeof value !== 'object') {
24
+ return false;
25
+ }
26
+ const manifest = value;
27
+ return (typeof manifest.templateVersion === 'string' &&
28
+ !!manifest.templates &&
29
+ typeof manifest.templates === 'object' &&
30
+ Object.values(manifest.templates).every((template) => !!template &&
31
+ typeof template === 'object' &&
32
+ typeof template.version === 'string' &&
33
+ typeof template.description === 'string' &&
34
+ Array.isArray(template.changes) &&
35
+ template.changes.every((change) => typeof change === 'string')));
36
+ }
21
37
  export function renderTemplateForSync({ templateRelativePath, currentPath, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers = [], }) {
22
38
  let contents = readTemplate(templateRelativePath);
23
- const mcpConfigAgentId = getMcpConfigAgentId(templateRelativePath);
39
+ const mcpConfigAgentId = resolveMcpConfigAgentId(templateRelativePath);
24
40
  if (mcpConfigAgentId) {
25
41
  return renderMcpConfig({ agentId: mcpConfigAgentId, baseContents: contents, selectedMcpServers });
26
42
  }
@@ -31,6 +47,31 @@ export function renderTemplateForSync({ templateRelativePath, currentPath, selec
31
47
  contents = renderWorkerToolboxRoutingPlaceholders(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills);
32
48
  return contents;
33
49
  }
50
+ export function renderMissingWorkflowFiles({ missingTargets, overview, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers = [], }) {
51
+ return missingTargets.map((target) => {
52
+ let contents = readTemplate(target.templateRelativePath);
53
+ if (target.mcpConfigAgentId && (target.selectedMcpServers?.length || selectedMcpServers.length)) {
54
+ contents = renderMcpConfig({
55
+ agentId: target.mcpConfigAgentId,
56
+ baseContents: contents,
57
+ selectedMcpServers: target.selectedMcpServers ?? selectedMcpServers,
58
+ });
59
+ }
60
+ if (target.requiresProjectOverview) {
61
+ if (!overview?.trim()) {
62
+ throw new Error('Project overview is required to create AGENTS.md.');
63
+ }
64
+ contents = contents.replace('{{PROJECT_OVERVIEW}}', overview.trim());
65
+ }
66
+ contents = renderSkillRouting(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills);
67
+ contents = renderWorkerToolboxRoutingPlaceholders(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills);
68
+ return {
69
+ label: target.label,
70
+ targetPath: target.targetPath,
71
+ contents,
72
+ };
73
+ });
74
+ }
34
75
  export function renderSkillRouting(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = []) {
35
76
  if (!contents.includes('{{OPTIONAL_SKILL_ROUTING}}')) {
36
77
  return contents;
@@ -85,15 +126,6 @@ export function renderWorkerToolboxRoutingPlaceholders(contents, selectedLanguag
85
126
  }
86
127
  return renderedContents;
87
128
  }
88
- export function renderMcpConfig({ agentId, baseContents, selectedMcpServers, }) {
89
- if (selectedMcpServers.length === 0) {
90
- return baseContents ?? renderJsonMcpConfig({});
91
- }
92
- if (agentId === 'codex') {
93
- return renderCodexMcpConfig(baseContents ?? '', selectedMcpServers);
94
- }
95
- return renderJsonMcpConfig(buildJsonMcpServerConfigs(agentId, selectedMcpServers), baseContents);
96
- }
97
129
  function collectWorkerRelevantSkills({ selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, }) {
98
130
  const implementationRelevantWorkflowSkills = selectedWorkflowSkills.filter((skill) => skill.id === 'ai-prompt-engineer-master' || skill.id === 'ux-expert');
99
131
  return [
@@ -120,108 +152,3 @@ export function extractProjectOverview(filePath) {
120
152
  const overview = match?.[1]?.trim();
121
153
  return overview || undefined;
122
154
  }
123
- function renderCodexMcpConfig(baseContents, selectedMcpServers) {
124
- const trimmedBaseContents = baseContents.trimEnd();
125
- const serverConfigs = selectedMcpServers.map((server) => buildCodexMcpServerConfig(server)).filter((config) => config.length > 0);
126
- if (serverConfigs.length === 0) {
127
- return baseContents;
128
- }
129
- const separator = trimmedBaseContents ? '\n\n' : '';
130
- return `${trimmedBaseContents}${separator}${serverConfigs.join('\n\n')}\n`;
131
- }
132
- function buildCodexMcpServerConfig(server) {
133
- if (server.id === 'github') {
134
- return `[mcp_servers.github]
135
- url = "https://api.githubcopilot.com/mcp/"
136
- bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
137
-
138
- [mcp_servers.github.tools.issue_write]
139
- approval_mode = "approve"
140
-
141
- [mcp_servers.github.tools.sub_issue_write]
142
- approval_mode = "approve"`;
143
- }
144
- if (server.id === 'notion') {
145
- return `[mcp_servers.notion]
146
- url = "https://mcp.notion.com/mcp"`;
147
- }
148
- return '';
149
- }
150
- function getMcpConfigAgentId(templateRelativePath) {
151
- if (templateRelativePath === '.codex/config.toml') {
152
- return 'codex';
153
- }
154
- if (templateRelativePath === 'mcp/claude/.mcp.json') {
155
- return 'claude';
156
- }
157
- if (templateRelativePath === 'mcp/gemini/settings.json') {
158
- return 'gemini';
159
- }
160
- if (templateRelativePath === '.gemini/settings.json') {
161
- return 'gemini';
162
- }
163
- return undefined;
164
- }
165
- function buildJsonMcpServerConfigs(agentId, selectedMcpServers) {
166
- const mcpServers = {};
167
- for (const server of selectedMcpServers) {
168
- if (server.id === 'github') {
169
- mcpServers.github = buildGithubMcpServerConfig(agentId);
170
- }
171
- if (server.id === 'notion') {
172
- mcpServers.notion = buildNotionMcpServerConfig();
173
- }
174
- }
175
- return mcpServers;
176
- }
177
- function buildGithubMcpServerConfig(agentId) {
178
- const headers = {
179
- Authorization: 'Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}',
180
- };
181
- if (agentId === 'gemini') {
182
- return {
183
- httpUrl: 'https://api.githubcopilot.com/mcp/',
184
- headers,
185
- };
186
- }
187
- return {
188
- type: 'http',
189
- url: 'https://api.githubcopilot.com/mcp/',
190
- headers,
191
- };
192
- }
193
- function buildNotionMcpServerConfig() {
194
- return {
195
- url: 'https://mcp.notion.com/mcp',
196
- };
197
- }
198
- function renderJsonMcpConfig(mcpServers, baseContents) {
199
- const baseConfig = parseJsonObject(baseContents);
200
- baseConfig.mcpServers = mcpServers;
201
- return `${JSON.stringify(baseConfig, null, 2)}\n`;
202
- }
203
- function parseJsonObject(contents) {
204
- if (!contents?.trim()) {
205
- return {};
206
- }
207
- const parsed = JSON.parse(contents);
208
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
209
- throw new Error('Expected JSON template to contain an object.');
210
- }
211
- return parsed;
212
- }
213
- function isTemplateManifest(value) {
214
- if (!value || typeof value !== 'object') {
215
- return false;
216
- }
217
- const manifest = value;
218
- return (typeof manifest.templateVersion === 'string' &&
219
- !!manifest.templates &&
220
- typeof manifest.templates === 'object' &&
221
- Object.values(manifest.templates).every((template) => !!template &&
222
- typeof template === 'object' &&
223
- typeof template.version === 'string' &&
224
- typeof template.description === 'string' &&
225
- Array.isArray(template.changes) &&
226
- template.changes.every((change) => typeof change === 'string')));
227
- }
@@ -0,0 +1,6 @@
1
+ export { handleListSkills } from './list-skills/handler.js';
2
+ export { getNextSkillSelection, handleUseSkill } from './use-skill/handler.js';
3
+ export { handleStopManagingFile, stopSelectedSkill } from './stop-managing-file/handler.js';
4
+ export { getMcpServerListItems, handleListMcpServers } from './list-mcp-servers/handler.js';
5
+ export { getNextMcpSelection, handleUseMcpServer } from './use-mcp-server/handler.js';
6
+ 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 '../../template-catalog/index.js';
4
+ import { getProjectContext } from '../../../shared/paths.js';
5
+ import { renderIntro } from '../../../support/interactive-guidance/index.js';
6
+ import { readStateForDoctor } from '../../workflow-state-ledger/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,75 @@
1
+ import { intro, log, outro } from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_WORKFLOW_SKILLS } from '../../template-catalog/index.js';
4
+ import { getProjectContext } from '../../../shared/paths.js';
5
+ import { renderIntro } from '../../../support/interactive-guidance/index.js';
6
+ import { readStateForDoctor } from '../../workflow-state-ledger/index.js';
7
+ export async function handleListSkills(_command) {
8
+ await renderIntro();
9
+ intro(chalk.cyan('Available workflow skills'));
10
+ const { statePath } = getProjectContext();
11
+ const stateResult = readStateForDoctor(statePath);
12
+ const state = stateResult.ok ? stateResult.state : undefined;
13
+ if (!stateResult.ok) {
14
+ log.warn(`${stateResult.message} Showing available skills without project state.`);
15
+ log.info('Run `sibu init` before selecting project skills.');
16
+ }
17
+ logSkillGroup('Languages', getLanguageSkillItems(state));
18
+ logSkillGroup('Frameworks', getFrameworkSkillItems(state));
19
+ logSkillGroup('Databases', getDatabaseSkillItems(state));
20
+ logSkillGroup('Architecture', getArchitectureSkillItems(state));
21
+ logSkillGroup('Workflow', getWorkflowSkillItems(state));
22
+ outro(chalk.green('Skill list ready.'));
23
+ }
24
+ function getLanguageSkillItems(state) {
25
+ const selectedSkillIds = new Set(state?.selectedLanguageSkills ?? []);
26
+ return SELECTABLE_LANGUAGE_SKILLS.map((skill) => ({
27
+ name: skill.name,
28
+ id: skill.id,
29
+ description: skill.description,
30
+ selected: selectedSkillIds.has(skill.id),
31
+ }));
32
+ }
33
+ function getFrameworkSkillItems(state) {
34
+ const selectedSkillIds = new Set(state?.selectedFrameworkSkills ?? []);
35
+ return SELECTABLE_FRAMEWORK_SKILLS.map((skill) => ({
36
+ name: skill.name,
37
+ id: skill.id,
38
+ description: skill.description,
39
+ selected: selectedSkillIds.has(skill.id),
40
+ }));
41
+ }
42
+ function getDatabaseSkillItems(state) {
43
+ const selectedSkillIds = new Set(state?.selectedDatabaseSkills ?? []);
44
+ return SELECTABLE_DATABASE_SKILLS.map((skill) => ({
45
+ name: skill.name,
46
+ id: skill.id,
47
+ description: skill.description,
48
+ selected: selectedSkillIds.has(skill.id),
49
+ }));
50
+ }
51
+ function getArchitectureSkillItems(state) {
52
+ return SELECTABLE_ARCHITECTURE_SKILLS.map((skill) => ({
53
+ name: skill.name,
54
+ id: skill.id,
55
+ description: skill.description,
56
+ selected: state?.selectedArchitectureSkill === skill.id,
57
+ }));
58
+ }
59
+ function getWorkflowSkillItems(state) {
60
+ const selectedSkillIds = new Set(state?.selectedWorkflowSkills ?? []);
61
+ return SELECTABLE_WORKFLOW_SKILLS.map((skill) => ({
62
+ name: skill.name,
63
+ id: skill.id,
64
+ description: skill.description,
65
+ selected: selectedSkillIds.has(skill.id),
66
+ }));
67
+ }
68
+ function logSkillGroup(label, skills) {
69
+ log.message(chalk.bold(label));
70
+ for (const skill of skills) {
71
+ const marker = skill.selected ? chalk.green('● selected') : chalk.dim('○ available');
72
+ console.log(` ${marker} ${chalk.bold(skill.name)} ${chalk.dim(`(${skill.id})`)}`);
73
+ console.log(` ${chalk.dim(skill.description)}`);
74
+ }
75
+ }
@@ -0,0 +1,221 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { cancel, intro, isCancel, log, outro, select } from '@clack/prompts';
4
+ import chalk from 'chalk';
5
+ import { STATE_RELATIVE_PATH } from '../../workflow-state-ledger/state-path.js';
6
+ import { resolveSelectableSkillById } from '../../template-catalog/index.js';
7
+ import { readFileHashIfPresent, sha256 } from '../../../shared/hash.js';
8
+ import { getProjectContext } from '../../../shared/paths.js';
9
+ import { renderIntro } from '../../../support/interactive-guidance/index.js';
10
+ import { cloneState, readStateForDoctor, writeStateFile } from '../../workflow-state-ledger/index.js';
11
+ import { getTemplateVersion, readTemplateManifest } from '../../template-catalog/index.js';
12
+ import { renderTemplateForSync } from '../../template-catalog/index.js';
13
+ import { removeUndefinedFields } from '../../../shared/object.js';
14
+ import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedWorkflowSkillsFromState, getSkillTargetsForAgents, } from '../../template-catalog/index.js';
15
+ export async function handleStopManagingFile({ skillName }) {
16
+ await renderIntro();
17
+ intro(chalk.cyan('Updating skill management'));
18
+ const { rootPath, statePath } = getProjectContext();
19
+ const stateResult = readStateForDoctor(statePath);
20
+ if (!stateResult.ok) {
21
+ log.error(stateResult.message);
22
+ log.info('Run `sibu init` before managing workflow skill state.');
23
+ outro(chalk.yellow('Skill update unavailable.'));
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const result = stopSelectedSkill({ rootPath, state: stateResult.state, skillName });
28
+ switch (result.status) {
29
+ case 'blocked':
30
+ log.error(result.message);
31
+ if (result.hint) {
32
+ log.info(result.hint);
33
+ }
34
+ outro(chalk.yellow('Nothing changed.'));
35
+ process.exitCode = 1;
36
+ return;
37
+ case 'noop':
38
+ log.success(result.message);
39
+ log.info('No files changed.');
40
+ outro(chalk.green('Skill selection is already up to date.'));
41
+ return;
42
+ case 'stopped':
43
+ writeStateFile(statePath, result.state);
44
+ for (const stoppedPath of result.stoppedPaths) {
45
+ log.success(`Stopped managing ${stoppedPath.relativePath}.`);
46
+ }
47
+ log.success('Updated AGENTS.md skill routing.');
48
+ log.success(`Updated ${STATE_RELATIVE_PATH}.`);
49
+ for (const [index, stoppedPath] of result.stoppedPaths.entries()) {
50
+ await askToDeleteStoppedFile(stoppedPath, result.stoppedFiles[index]);
51
+ }
52
+ outro(chalk.green(`Updated ${result.skillName}.`));
53
+ return;
54
+ }
55
+ }
56
+ export function stopSelectedSkill({ rootPath, state, skillName }) {
57
+ const resolution = resolveSelectableSkillById(skillName);
58
+ if (!resolution.ok) {
59
+ return { status: 'blocked', message: resolution.message, hint: 'Use `sibu skills stop <skill_name>` with a selectable skill id from `sibu skills list`.' };
60
+ }
61
+ if (!isSkillSelected(state, resolution.resolved)) {
62
+ return { status: 'noop', message: `${resolution.resolved.skill.name} is not selected.` };
63
+ }
64
+ const stoppedPaths = getSkillManagedPaths(rootPath, state, resolution.resolved);
65
+ if (stoppedPaths.length === 0) {
66
+ return { status: 'blocked', message: `${resolution.resolved.skill.name} does not have a managed target for the selected agents.` };
67
+ }
68
+ const nextState = cloneState(state);
69
+ const stoppedFiles = [];
70
+ for (const stoppedPath of stoppedPaths) {
71
+ const managedFile = nextState.managedFiles[stoppedPath.relativePath];
72
+ if (!managedFile) {
73
+ return {
74
+ status: 'blocked',
75
+ message: `${stoppedPath.relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
76
+ hint: 'Run `sibu sync` to review workflow state before stopping this skill.',
77
+ };
78
+ }
79
+ const currentHash = readFileHashIfPresent(stoppedPath.absolutePath);
80
+ const stoppedFile = removeUndefinedFields({
81
+ ...managedFile,
82
+ sha256: currentHash ?? managedFile.sha256,
83
+ status: 'unmanaged',
84
+ reason: 'Stopped by `sibu skills stop`.',
85
+ });
86
+ nextState.managedFiles[stoppedPath.relativePath] = stoppedFile;
87
+ stoppedFiles.push(stoppedFile);
88
+ }
89
+ removeSelectedSkill(nextState, resolution.resolved);
90
+ const agentsUpdate = getAgentsUpdate(rootPath, nextState);
91
+ if (!agentsUpdate.ok) {
92
+ return { status: 'blocked', message: agentsUpdate.message, hint: 'Run `sibu sync` to review workflow state before stopping this skill.' };
93
+ }
94
+ fs.writeFileSync(agentsUpdate.path, agentsUpdate.contents, 'utf8');
95
+ const manifest = readTemplateManifest();
96
+ nextState.templateVersion = manifest.templateVersion;
97
+ nextState.updatedAt = new Date().toISOString();
98
+ nextState.managedFiles['AGENTS.md'] = removeUndefinedFields({
99
+ ...agentsUpdate.managedFile,
100
+ templateVersion: getTemplateVersion(manifest, agentsUpdate.managedFile.template),
101
+ sha256: sha256(agentsUpdate.contents),
102
+ status: agentsUpdate.managedFile.status ?? 'managed',
103
+ });
104
+ return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, skillName: resolution.resolved.skill.name };
105
+ }
106
+ function isSkillSelected(state, resolved) {
107
+ switch (resolved.kind) {
108
+ case 'language':
109
+ return state.selectedLanguageSkills?.includes(resolved.skill.id) ?? false;
110
+ case 'framework':
111
+ return state.selectedFrameworkSkills?.includes(resolved.skill.id) ?? false;
112
+ case 'architecture':
113
+ return state.selectedArchitectureSkill === resolved.skill.id;
114
+ case 'database':
115
+ return state.selectedDatabaseSkills?.includes(resolved.skill.id) ?? false;
116
+ case 'workflow':
117
+ return state.selectedWorkflowSkills?.includes(resolved.skill.id) ?? false;
118
+ }
119
+ }
120
+ function removeSelectedSkill(state, resolved) {
121
+ switch (resolved.kind) {
122
+ case 'language':
123
+ state.selectedLanguageSkills = (state.selectedLanguageSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
124
+ return;
125
+ case 'framework':
126
+ state.selectedFrameworkSkills = (state.selectedFrameworkSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
127
+ return;
128
+ case 'architecture':
129
+ delete state.selectedArchitectureSkill;
130
+ return;
131
+ case 'database':
132
+ state.selectedDatabaseSkills = (state.selectedDatabaseSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
133
+ return;
134
+ case 'workflow':
135
+ state.selectedWorkflowSkills = (state.selectedWorkflowSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
136
+ return;
137
+ }
138
+ }
139
+ function getSkillManagedPaths(rootPath, state, resolved) {
140
+ const relativePaths = new Set(getSkillTargetsForAgents(resolved.skill, getSelectedAgentsFromState(state)).map((target) => target.targetRelativePath));
141
+ return [...relativePaths].map((relativePath) => ({
142
+ relativePath,
143
+ absolutePath: path.join(rootPath, relativePath),
144
+ }));
145
+ }
146
+ function getAgentsUpdate(rootPath, state) {
147
+ const agentsRelativePath = 'AGENTS.md';
148
+ const agentsPath = path.join(rootPath, agentsRelativePath);
149
+ const managedFile = state.managedFiles[agentsRelativePath];
150
+ if (!managedFile) {
151
+ return { ok: false, message: 'AGENTS.md is not recorded in `.sibu/state.json`.' };
152
+ }
153
+ if (!fs.existsSync(agentsPath)) {
154
+ return { ok: false, message: 'AGENTS.md is missing.' };
155
+ }
156
+ const currentHash = sha256(fs.readFileSync(agentsPath, 'utf8'));
157
+ if (currentHash !== managedFile.sha256) {
158
+ return { ok: false, message: 'AGENTS.md has changed since Sibu last recorded it.' };
159
+ }
160
+ return {
161
+ ok: true,
162
+ path: agentsPath,
163
+ managedFile,
164
+ contents: renderTemplateForSync({
165
+ templateRelativePath: managedFile.template,
166
+ currentPath: agentsPath,
167
+ selectedLanguageSkills: getSelectedLanguageSkillsFromState(state),
168
+ selectedFrameworkSkills: getSelectedFrameworkSkillsFromState(state),
169
+ selectedArchitectureSkill: getSelectedArchitectureSkillFromState(state),
170
+ selectedWorkflowSkills: getSelectedWorkflowSkillsFromState(state),
171
+ selectedDatabaseSkills: getSelectedDatabaseSkillsFromState(state),
172
+ }),
173
+ };
174
+ }
175
+ async function askToDeleteStoppedFile(managedPath, managedFile) {
176
+ if (!fs.existsSync(managedPath.absolutePath)) {
177
+ log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
178
+ return;
179
+ }
180
+ if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
181
+ log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
182
+ return;
183
+ }
184
+ const deleteAction = await select({
185
+ message: `Do you also want to delete ${managedPath.relativePath} for good?`,
186
+ options: [
187
+ { value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local file unchanged.' },
188
+ { value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
189
+ ],
190
+ });
191
+ if (isCancel(deleteAction)) {
192
+ cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
193
+ process.exit(0);
194
+ }
195
+ if (deleteAction === 'keep') {
196
+ log.info(`Kept ${managedPath.relativePath}.`);
197
+ return;
198
+ }
199
+ if (hasLocalEdits(managedPath.absolutePath, managedFile)) {
200
+ const confirmDelete = await select({
201
+ message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
202
+ options: [
203
+ { value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
204
+ { value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
205
+ ],
206
+ });
207
+ if (isCancel(confirmDelete)) {
208
+ cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
209
+ process.exit(0);
210
+ }
211
+ if (confirmDelete === 'keep') {
212
+ log.info(`Kept ${managedPath.relativePath}.`);
213
+ return;
214
+ }
215
+ }
216
+ fs.unlinkSync(managedPath.absolutePath);
217
+ log.success(`Deleted ${managedPath.relativePath}.`);
218
+ }
219
+ function hasLocalEdits(filePath, managedFile) {
220
+ return readFileHashIfPresent(filePath) !== managedFile.sha256;
221
+ }