@juancr11/sibu 0.13.2 → 0.15.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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline/promises';
3
- import { execFileSync } from 'node:child_process';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { previewAndConfirmMaintainerRelease } from '../modules/maintainer-release-support/release-workflow/handler.js';
@@ -166,6 +166,20 @@ function createTerminalPorts() {
166
166
  return commandFailure(error);
167
167
  }
168
168
  },
169
+ runInteractive(command, args) {
170
+ const result = spawnSync(command, args, {
171
+ stdio: 'inherit',
172
+ });
173
+ if (result.error) {
174
+ return {
175
+ exitCode: 1,
176
+ stderr: result.error.message,
177
+ };
178
+ }
179
+ return {
180
+ exitCode: result.status ?? 1,
181
+ };
182
+ },
169
183
  };
170
184
  }
171
185
  function printExecutionResult(result, ports) {
@@ -1 +1 @@
1
- export { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForNotionDocsParentPage, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, MCP_SERVER_SELECTION_MESSAGE, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
1
+ export { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForNotionDocsParentPage, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, MCP_SERVER_SELECTION_MESSAGE, getPromptableWorkflowSkills, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
@@ -148,11 +148,16 @@ export async function askForArchitectureSkill() {
148
148
  }
149
149
  return SELECTABLE_ARCHITECTURE_SKILLS.find((skill) => skill.id === selectedArchitectureSkillId);
150
150
  }
151
- export async function askForWorkflowSkills() {
151
+ export function getPromptableWorkflowSkills(excludedWorkflowSkillIds = []) {
152
+ const excludedIds = new Set(excludedWorkflowSkillIds);
153
+ return SELECTABLE_WORKFLOW_SKILLS.filter((skill) => !excludedIds.has(skill.id));
154
+ }
155
+ export async function askForWorkflowSkills(excludedWorkflowSkillIds = []) {
156
+ const promptableWorkflowSkills = getPromptableWorkflowSkills(excludedWorkflowSkillIds);
152
157
  const selectedWorkflowSkillIds = await multiselect({
153
158
  message: 'Select optional workflow skills for this project.',
154
159
  required: false,
155
- options: SELECTABLE_WORKFLOW_SKILLS.map((skill) => ({
160
+ options: promptableWorkflowSkills.map((skill) => ({
156
161
  value: skill.id,
157
162
  label: skill.name,
158
163
  hint: skill.description,
@@ -162,7 +167,7 @@ export async function askForWorkflowSkills() {
162
167
  cancel('Initialization cancelled.');
163
168
  process.exit(0);
164
169
  }
165
- return SELECTABLE_WORKFLOW_SKILLS.filter((skill) => selectedWorkflowSkillIds.includes(skill.id));
170
+ return promptableWorkflowSkills.filter((skill) => selectedWorkflowSkillIds.includes(skill.id));
166
171
  }
167
172
  export function shouldAskForNewLanguageSkills(state) {
168
173
  return (state.selectedLanguageSkills?.length ?? 0) === 0;
@@ -207,8 +207,9 @@ export async function executeConfirmedRelease(plan, ports) {
207
207
  }
208
208
  const npmPublishArgs = buildNpmPublishArgs(plan);
209
209
  const npmPublishLog = plan.hasOtp ? 'npm publish --access public --otp ******' : 'npm publish --access public';
210
- printExecutionProgress(ports, `Publishing package to npm: ${npmPublishLog}...`);
211
- const npmPublish = await ports.run('npm', npmPublishArgs);
210
+ const npmPublishMode = plan.hasOtp ? 'captured OTP publish' : 'interactive browser/2FA-capable publish';
211
+ printExecutionProgress(ports, `Publishing package to npm: ${npmPublishLog} (${npmPublishMode})...`);
212
+ const npmPublish = await runNpmPublish(ports, npmPublishArgs, plan);
212
213
  if (npmPublish.exitCode !== 0) {
213
214
  const failure = failExecution(completedSteps, 'publish-npm', 'Publishing to npm failed.', npmPublish);
214
215
  printExecutionFailure(ports, failure);
@@ -272,6 +273,12 @@ function buildNpmPublishArgs(plan) {
272
273
  }
273
274
  return args;
274
275
  }
276
+ function runNpmPublish(ports, args, plan) {
277
+ if (plan.hasOtp || !ports.runInteractive) {
278
+ return ports.run('npm', args);
279
+ }
280
+ return ports.runInteractive('npm', args);
281
+ }
275
282
  async function runStep(ports, completedSteps, stepName, startMessage, command, args, completedMessage) {
276
283
  printExecutionProgress(ports, startMessage);
277
284
  const result = await ports.run(command, args);
@@ -2,10 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { log } from '@clack/prompts';
4
4
  import { STATE_RELATIVE_PATH } from '../../../shared/catalog.js';
5
+ import { sha256 } from '../../../shared/hash.js';
5
6
  import { getProjectContext } from '../../../shared/paths.js';
6
7
  import { askForNotionDocsParentPage } from '../../interactive-guidance/index.js';
8
+ import { renderTemplateForSync } from '../../template-catalog-rendering/index.js';
7
9
  import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
8
- import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, renderMissingWorkflowFiles, resolveSelectableMcpServerById, writeSibuState, } from '../../workflow-target-planning/index.js';
10
+ import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowSkillsImpliedByMcpServers, getWorkflowTargets, renderMissingWorkflowFiles, resolveSelectableMcpServerById, writeSibuState, } from '../../workflow-target-planning/index.js';
9
11
  const defaultDependencies = {
10
12
  askForNotionDocsParentPage,
11
13
  };
@@ -71,11 +73,13 @@ function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, m
71
73
  const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
72
74
  const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
73
75
  const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
74
- const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
76
+ const currentWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
77
+ const impliedWorkflowSkills = getWorkflowSkillsImpliedByMcpServers(selectionResult.selectedMcpServers.map((server) => server.id));
78
+ const selectedWorkflowSkills = mergeWorkflowSkills(currentWorkflowSkills, impliedWorkflowSkills);
75
79
  const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
76
- const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
80
+ const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, currentWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
77
81
  const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.selectedMcpServers);
78
- const affectedTargets = getAffectedMcpTargets(previousTargets, targets);
82
+ const affectedTargets = getAffectedTargets(previousTargets, targets);
79
83
  const preflightError = getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets });
80
84
  if (preflightError) {
81
85
  log.error(preflightError);
@@ -83,8 +87,13 @@ function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, m
83
87
  process.exitCode = 1;
84
88
  return;
85
89
  }
90
+ const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
91
+ if (!agentsTarget) {
92
+ throw new Error('AGENTS.md target is missing from workflow targets.');
93
+ }
94
+ const nonAgentTargets = affectedTargets.filter((target) => target.label !== 'AGENTS.md');
86
95
  const files = renderMissingWorkflowFiles({
87
- missingTargets: affectedTargets,
96
+ missingTargets: nonAgentTargets,
88
97
  selectedLanguageSkills,
89
98
  selectedFrameworkSkills,
90
99
  selectedArchitectureSkill,
@@ -95,9 +104,23 @@ function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, m
95
104
  for (const file of files) {
96
105
  const fileAlreadyExists = fs.existsSync(file.targetPath);
97
106
  fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
98
- fs.writeFileSync(file.targetPath, file.contents, 'utf8');
107
+ fs.writeFileSync(file.targetPath, file.contents, fileAlreadyExists ? 'utf8' : { encoding: 'utf8', flag: 'wx' });
99
108
  log.success(`${fileAlreadyExists ? 'Updated' : 'Created'} ${file.label}`);
100
109
  }
110
+ if (affectedTargets.some((target) => target.label === 'AGENTS.md')) {
111
+ const agentsContents = renderTemplateForSync({
112
+ templateRelativePath: agentsTarget.templateRelativePath,
113
+ currentPath: agentsTarget.targetPath,
114
+ selectedLanguageSkills,
115
+ selectedFrameworkSkills,
116
+ selectedArchitectureSkill,
117
+ selectedWorkflowSkills,
118
+ selectedDatabaseSkills,
119
+ selectedMcpServers: selectionResult.selectedMcpServers,
120
+ });
121
+ fs.writeFileSync(agentsTarget.targetPath, agentsContents, 'utf8');
122
+ log.success('Updated AGENTS.md skill routing');
123
+ }
101
124
  writeSibuState({
102
125
  rootPath,
103
126
  statePath,
@@ -113,20 +136,45 @@ function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, m
113
136
  });
114
137
  log.success(`Updated ${STATE_RELATIVE_PATH}`);
115
138
  log.success(`Added ${selectionResult.serverName}.`);
139
+ for (const skill of impliedWorkflowSkills.filter((skill) => !currentWorkflowSkills.some((currentSkill) => currentSkill.id === skill.id))) {
140
+ log.success(`Added ${skill.name}.`);
141
+ }
116
142
  log.info('Sibu configured MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
117
143
  }
118
- function getAffectedMcpTargets(previousTargets, targets) {
144
+ function getAffectedTargets(previousTargets, targets) {
119
145
  const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
120
- return targets.filter((target) => target.mcpConfigAgentId || (!previousTargetPaths.has(target.targetPath) && target.targetKind === 'mcp-config'));
146
+ const newTargets = targets.filter((target) => !previousTargetPaths.has(target.targetPath) && (target.targetKind === 'skill' || target.targetKind === 'mcp-config'));
147
+ const mcpConfigTargets = targets.filter((target) => target.mcpConfigAgentId);
148
+ const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
149
+ const affectedTargets = new Map();
150
+ for (const target of [...mcpConfigTargets, ...newTargets, ...(newTargets.some((target) => target.targetKind === 'skill') && agentsTarget ? [agentsTarget] : [])]) {
151
+ affectedTargets.set(target.targetPath, target);
152
+ }
153
+ return [...affectedTargets.values()];
154
+ }
155
+ function mergeWorkflowSkills(...skillGroups) {
156
+ const skillsById = new Map();
157
+ for (const skill of skillGroups.flat()) {
158
+ skillsById.set(skill.id, skill);
159
+ }
160
+ return [...skillsById.values()];
121
161
  }
122
162
  function getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets, }) {
123
163
  const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
124
164
  for (const target of affectedTargets) {
125
165
  const relativePath = path.relative(rootPath, target.targetPath);
126
166
  if (previousTargetPaths.has(target.targetPath)) {
127
- if (!state.managedFiles[relativePath]) {
167
+ const managedFile = state.managedFiles[relativePath];
168
+ if (!managedFile) {
128
169
  return `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`;
129
170
  }
171
+ if (!fs.existsSync(target.targetPath)) {
172
+ return `${relativePath} is missing.`;
173
+ }
174
+ const currentHash = sha256(fs.readFileSync(target.targetPath, 'utf8'));
175
+ if (currentHash !== managedFile.sha256) {
176
+ return `${relativePath} has changed since Sibu last recorded it.`;
177
+ }
130
178
  continue;
131
179
  }
132
180
  if (fs.existsSync(target.targetPath)) {
@@ -6,7 +6,14 @@ import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
6
6
  import { getProjectContext } from '../../shared/paths.js';
7
7
  import { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForNotionDocsParentPage, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro, } from '../interactive-guidance/index.js';
8
8
  import { readStateForDoctor } from '../workflow-state-registry/index.js';
9
- import { getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../workflow-target-planning/index.js';
9
+ import { getWorkflowSkillsImpliedByMcpServers, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../workflow-target-planning/index.js';
10
+ function mergeWorkflowSkills(...skillGroups) {
11
+ const skillsById = new Map();
12
+ for (const skill of skillGroups.flat()) {
13
+ skillsById.set(skill.id, skill);
14
+ }
15
+ return [...skillsById.values()];
16
+ }
10
17
  const defaultDependencies = {
11
18
  renderIntro,
12
19
  askForSupportedAgents,
@@ -41,12 +48,16 @@ export async function handleInitProject(_command, dependencies = defaultDependen
41
48
  }
42
49
  const selectedAgents = await dependencies.askForSupportedAgents();
43
50
  const selectedMcpServers = await dependencies.askForMcpServers();
51
+ const impliedWorkflowSkills = getWorkflowSkillsImpliedByMcpServers(selectedMcpServers.map((server) => server.id));
52
+ for (const skill of impliedWorkflowSkills) {
53
+ log.info(`${skill.name} will be included because its matching MCP server was selected.`);
54
+ }
44
55
  const notionDocsParentPage = selectedMcpServers.some((server) => server.id === 'notion') ? await dependencies.askForNotionDocsParentPage() : undefined;
45
56
  const selectedLanguageSkills = await dependencies.askForLanguageSkills();
46
57
  const selectedFrameworkSkills = await dependencies.askForFrameworkSkills();
47
58
  const selectedDatabaseSkills = await dependencies.askForDatabaseSkills();
48
59
  const selectedArchitectureSkill = await dependencies.askForArchitectureSkill();
49
- const selectedWorkflowSkills = await dependencies.askForWorkflowSkills();
60
+ const selectedWorkflowSkills = mergeWorkflowSkills(impliedWorkflowSkills, await dependencies.askForWorkflowSkills(impliedWorkflowSkills.map((skill) => skill.id)));
50
61
  const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
51
62
  const missingTargets = targets.filter((target) => !fs.existsSync(target.targetPath));
52
63
  log.message('I will create this project’s initial AI workflow files.');
@@ -2,13 +2,17 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { log } from '@clack/prompts';
4
4
  import { STATE_RELATIVE_PATH } from '../../../shared/catalog.js';
5
- import { resolveSelectableSkillById } from '../../workflow-target-planning/index.js';
5
+ import { askForNotionDocsParentPage } from '../../interactive-guidance/index.js';
6
+ import { getMcpServersRequiredByWorkflowSkills, resolveSelectableSkillById } from '../../workflow-target-planning/index.js';
6
7
  import { sha256 } from '../../../shared/hash.js';
7
8
  import { getProjectContext } from '../../../shared/paths.js';
8
9
  import { renderTemplateForSync } from '../../template-catalog-rendering/index.js';
9
10
  import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
10
- import { getSelectedAgentsFromState, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../../workflow-target-planning/index.js';
11
- export async function handleUseSkill(command) {
11
+ import { getSelectedAgentsFromState, getSelectedMcpServersFromState, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../../workflow-target-planning/index.js';
12
+ const defaultDependencies = {
13
+ askForNotionDocsParentPage,
14
+ };
15
+ export async function handleUseSkill(command, dependencies = defaultDependencies) {
12
16
  const { rootPath, statePath } = getProjectContext();
13
17
  const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
14
18
  if (!readiness.ok) {
@@ -34,7 +38,7 @@ export async function handleUseSkill(command) {
34
38
  process.exitCode = 1;
35
39
  return;
36
40
  case 'selected':
37
- applySelectedSkill({ rootPath, statePath, state: readiness.state, selectionResult });
41
+ await applySelectedSkill({ rootPath, statePath, state: readiness.state, selectionResult, dependencies });
38
42
  return;
39
43
  }
40
44
  }
@@ -132,7 +136,7 @@ export function getNextSkillSelection(state, skillName) {
132
136
  };
133
137
  }
134
138
  }
135
- function applySelectedSkill({ rootPath, statePath, state, selectionResult, }) {
139
+ async function applySelectedSkill({ rootPath, statePath, state, selectionResult, dependencies, }) {
136
140
  const plan = buildSkillApplicationPlan({ rootPath, state, selectionResult });
137
141
  const preflightError = getSkillApplicationPreflightError({ rootPath, state, plan });
138
142
  if (preflightError) {
@@ -141,25 +145,37 @@ function applySelectedSkill({ rootPath, statePath, state, selectionResult, }) {
141
145
  process.exitCode = 1;
142
146
  return;
143
147
  }
144
- const agentsContents = renderTemplateForSync({
145
- templateRelativePath: plan.agentsTarget.templateRelativePath,
146
- currentPath: plan.agentsTarget.targetPath,
148
+ const mcpServerConfigs = await getNextMcpServerConfigs({ state, selectedMcpServers: plan.selectedMcpServers, dependencies });
149
+ const dependencyMcpServers = plan.selectedMcpServers.filter((server) => !(state.selectedMcpServers ?? []).includes(server.id));
150
+ for (const server of dependencyMcpServers) {
151
+ log.info(`${selectionResult.skillName} requires ${server.name}. I will add them together.`);
152
+ }
153
+ const nonAgentTargets = plan.affectedTargets.filter((target) => target.label !== 'AGENTS.md');
154
+ const files = renderMissingWorkflowFiles({
155
+ missingTargets: nonAgentTargets,
147
156
  selectedLanguageSkills: selectionResult.selection.selectedLanguageSkills,
148
157
  selectedFrameworkSkills: selectionResult.selection.selectedFrameworkSkills,
149
158
  selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
150
159
  selectedWorkflowSkills: selectionResult.selection.selectedWorkflowSkills,
151
160
  selectedDatabaseSkills: selectionResult.selection.selectedDatabaseSkills,
161
+ selectedMcpServers: plan.selectedMcpServers,
152
162
  });
153
- const [newSkillFile] = renderMissingWorkflowFiles({
154
- missingTargets: [plan.newSkillTarget],
163
+ for (const file of files) {
164
+ const fileAlreadyExists = fs.existsSync(file.targetPath);
165
+ fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
166
+ fs.writeFileSync(file.targetPath, file.contents, fileAlreadyExists ? 'utf8' : { encoding: 'utf8', flag: 'wx' });
167
+ log.success(`${fileAlreadyExists ? 'Updated' : 'Created'} ${file.label}`);
168
+ }
169
+ const agentsContents = renderTemplateForSync({
170
+ templateRelativePath: plan.agentsTarget.templateRelativePath,
171
+ currentPath: plan.agentsTarget.targetPath,
155
172
  selectedLanguageSkills: selectionResult.selection.selectedLanguageSkills,
156
173
  selectedFrameworkSkills: selectionResult.selection.selectedFrameworkSkills,
157
174
  selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
175
+ selectedWorkflowSkills: selectionResult.selection.selectedWorkflowSkills,
158
176
  selectedDatabaseSkills: selectionResult.selection.selectedDatabaseSkills,
177
+ selectedMcpServers: plan.selectedMcpServers,
159
178
  });
160
- fs.mkdirSync(path.dirname(newSkillFile.targetPath), { recursive: true });
161
- fs.writeFileSync(newSkillFile.targetPath, newSkillFile.contents, { encoding: 'utf8', flag: 'wx' });
162
- log.success(`Created ${newSkillFile.label}`);
163
179
  fs.writeFileSync(plan.agentsTarget.targetPath, agentsContents, 'utf8');
164
180
  log.success('Updated AGENTS.md skill routing');
165
181
  writeSibuState({
@@ -171,43 +187,79 @@ function applySelectedSkill({ rootPath, statePath, state, selectionResult, }) {
171
187
  selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
172
188
  selectedWorkflowSkills: selectionResult.selection.selectedWorkflowSkills,
173
189
  selectedDatabaseSkills: selectionResult.selection.selectedDatabaseSkills,
190
+ selectedMcpServers: plan.selectedMcpServers,
191
+ mcpServerConfigs,
174
192
  targets: plan.targets,
175
193
  });
176
194
  log.success(`Updated ${STATE_RELATIVE_PATH}`);
177
195
  log.success(`Added ${selectionResult.skillName}.`);
196
+ for (const server of dependencyMcpServers) {
197
+ log.success(`Added ${server.name}.`);
198
+ }
178
199
  }
179
200
  function buildSkillApplicationPlan({ rootPath, state, selectionResult, }) {
180
201
  const selectedAgents = getSelectedAgentsFromState(state);
181
- const previousTargets = getWorkflowTargets(rootPath, selectedAgents, getSelectedLanguageSkillsFromState(state), getSelectedFrameworkSkillsFromState(state), getArchitectureSkillById(state.selectedArchitectureSkill), getSelectedWorkflowSkillsFromState(state), getSelectedDatabaseSkillsFromState(state));
182
- const targets = getWorkflowTargets(rootPath, selectedAgents, selectionResult.selection.selectedLanguageSkills, selectionResult.selection.selectedFrameworkSkills, selectionResult.selection.selectedArchitectureSkill, selectionResult.selection.selectedWorkflowSkills, selectionResult.selection.selectedDatabaseSkills);
202
+ const currentMcpServers = getSelectedMcpServersFromState(state);
203
+ const requiredMcpServers = getMcpServersRequiredByWorkflowSkills(selectionResult.selection.selectedWorkflowSkills.map((skill) => skill.id));
204
+ const selectedMcpServers = mergeMcpServers(currentMcpServers, requiredMcpServers);
205
+ const previousTargets = getWorkflowTargets(rootPath, selectedAgents, getSelectedLanguageSkillsFromState(state), getSelectedFrameworkSkillsFromState(state), getArchitectureSkillById(state.selectedArchitectureSkill), getSelectedWorkflowSkillsFromState(state), getSelectedDatabaseSkillsFromState(state), currentMcpServers);
206
+ const targets = getWorkflowTargets(rootPath, selectedAgents, selectionResult.selection.selectedLanguageSkills, selectionResult.selection.selectedFrameworkSkills, selectionResult.selection.selectedArchitectureSkill, selectionResult.selection.selectedWorkflowSkills, selectionResult.selection.selectedDatabaseSkills, selectedMcpServers);
183
207
  const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
184
- const newSkillTarget = targets.find((target) => !previousTargetPaths.has(target.targetPath));
208
+ const newTargets = targets.filter((target) => !previousTargetPaths.has(target.targetPath) && (target.targetKind === 'skill' || target.targetKind === 'mcp-config'));
209
+ const mcpConfigTargets = targets.filter((target) => target.mcpConfigAgentId && selectedMcpServers.length > currentMcpServers.length);
185
210
  const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
186
- if (!newSkillTarget) {
211
+ if (newTargets.length === 0) {
187
212
  throw new Error(`No new workflow target found for ${selectionResult.skillName}.`);
188
213
  }
189
214
  if (!agentsTarget) {
190
215
  throw new Error('AGENTS.md target is missing from workflow targets.');
191
216
  }
192
- return { agentsTarget, newSkillTarget, targets, selectedAgents };
217
+ const affectedTargets = dedupeTargets([...newTargets, ...mcpConfigTargets, agentsTarget]);
218
+ return { agentsTarget, newTargets, affectedTargets, targets, selectedAgents, selectedMcpServers };
193
219
  }
194
220
  function getSkillApplicationPreflightError({ rootPath, state, plan }) {
195
- if (fs.existsSync(plan.newSkillTarget.targetPath)) {
196
- return `${path.relative(rootPath, plan.newSkillTarget.targetPath)} already exists but is not recorded for this selection.`;
221
+ const existingRecordedTargets = plan.affectedTargets.filter((target) => state.managedFiles[path.relative(rootPath, target.targetPath)]);
222
+ const newTargets = plan.affectedTargets.filter((target) => !state.managedFiles[path.relative(rootPath, target.targetPath)]);
223
+ for (const target of newTargets) {
224
+ if (fs.existsSync(target.targetPath)) {
225
+ return `${path.relative(rootPath, target.targetPath)} already exists but is not recorded for this selection.`;
226
+ }
197
227
  }
198
- const agentsRelativePath = path.relative(rootPath, plan.agentsTarget.targetPath);
199
- const agentsManagedFile = state.managedFiles[agentsRelativePath];
200
- if (!agentsManagedFile) {
201
- return 'AGENTS.md is not recorded in `.sibu/state.json`.';
228
+ for (const target of existingRecordedTargets) {
229
+ const relativePath = path.relative(rootPath, target.targetPath);
230
+ const managedFile = state.managedFiles[relativePath];
231
+ if (!managedFile) {
232
+ return `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`;
233
+ }
234
+ if (!fs.existsSync(target.targetPath)) {
235
+ return `${relativePath} is missing.`;
236
+ }
237
+ const currentHash = sha256(fs.readFileSync(target.targetPath, 'utf8'));
238
+ if (currentHash !== managedFile.sha256) {
239
+ return `${relativePath} has changed since Sibu last recorded it.`;
240
+ }
202
241
  }
203
- if (!fs.existsSync(plan.agentsTarget.targetPath)) {
204
- return 'AGENTS.md is missing.';
242
+ return undefined;
243
+ }
244
+ function dedupeTargets(targets) {
245
+ return [...new Map(targets.map((target) => [target.targetPath, target])).values()];
246
+ }
247
+ function mergeMcpServers(...serverGroups) {
248
+ const serversById = new Map();
249
+ for (const server of serverGroups.flat()) {
250
+ serversById.set(server.id, server);
205
251
  }
206
- const agentsCurrentHash = sha256(fs.readFileSync(plan.agentsTarget.targetPath, 'utf8'));
207
- if (agentsCurrentHash !== agentsManagedFile.sha256) {
208
- return 'AGENTS.md has changed since Sibu last recorded it.';
252
+ return [...serversById.values()];
253
+ }
254
+ async function getNextMcpServerConfigs({ state, selectedMcpServers, dependencies, }) {
255
+ if (!selectedMcpServers.some((server) => server.id === 'notion') || state.selectedMcpServers?.includes('notion')) {
256
+ return state.mcpServerConfigs;
209
257
  }
210
- return undefined;
258
+ const docsParentPage = await dependencies.askForNotionDocsParentPage();
259
+ return {
260
+ ...state.mcpServerConfigs,
261
+ notion: { docsParentPage },
262
+ };
211
263
  }
212
264
  function getSelectedLanguageSkillsFromState(state) {
213
265
  return (state.selectedLanguageSkills ?? []).map(getLanguageSkillById);
@@ -13,6 +13,7 @@ export function applySyncAction({ rootPath, state, manifest, preview, action, })
13
13
  const currentTemplateVersion = preview.currentTemplateVersion ?? getTemplateVersion(manifest, managedFile.template);
14
14
  switch (action) {
15
15
  case 'apply-update': {
16
+ recordImpliedWorkflowSkill(nextState, preview);
16
17
  const contents = renderTemplateForSync({
17
18
  templateRelativePath: managedFile.template,
18
19
  currentPath: targetPath,
@@ -37,6 +38,7 @@ export function applySyncAction({ rootPath, state, manifest, preview, action, })
37
38
  return { state: nextState, changedFiles: true, changedState: true };
38
39
  }
39
40
  case 'mark-reviewed': {
41
+ recordImpliedWorkflowSkill(nextState, preview);
40
42
  const contents = fs.readFileSync(targetPath, 'utf8');
41
43
  nextState.templateVersion = manifest.templateVersion;
42
44
  nextState.updatedAt = new Date().toISOString();
@@ -85,3 +87,12 @@ export function applySyncAction({ rootPath, state, manifest, preview, action, })
85
87
  return { state, changedFiles: false, changedState: false };
86
88
  }
87
89
  }
90
+ function recordImpliedWorkflowSkill(state, preview) {
91
+ if (!preview.impliedWorkflowSkillId) {
92
+ return;
93
+ }
94
+ if (state.selectedWorkflowSkills?.includes(preview.impliedWorkflowSkillId)) {
95
+ return;
96
+ }
97
+ state.selectedWorkflowSkills = [...(state.selectedWorkflowSkills ?? []), preview.impliedWorkflowSkillId];
98
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { sha256 } from '../../shared/hash.js';
4
4
  import { hasReviewedTemplateVersion } from '../workflow-state-registry/index.js';
5
5
  import { renderTemplateForSync } from '../template-catalog-rendering/index.js';
6
- import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../workflow-target-planning/index.js';
6
+ import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowSkillsImpliedByMcpServers, getWorkflowTargets, } from '../workflow-target-planning/index.js';
7
7
  const ACTIONABLE_SYNC_PREVIEW_STATUSES = new Set([
8
8
  'new-template',
9
9
  'missing',
@@ -32,7 +32,19 @@ export function getSyncPreviews({ rootPath, state, manifest }) {
32
32
  const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
33
33
  const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
34
34
  const selectedMcpServers = getSelectedMcpServersFromState(state);
35
- const previews = Object.entries(state.managedFiles).map(([relativePath, managedFile]) => getManagedFileSyncPreview({
35
+ const impliedWorkflowSkills = getWorkflowSkillsImpliedByMcpServers(selectedMcpServers.map((server) => server.id));
36
+ const plannedWorkflowSkills = mergeWorkflowSkills(selectedWorkflowSkills, impliedWorkflowSkills);
37
+ const missingImpliedWorkflowSkills = impliedWorkflowSkills.filter((skill) => !selectedWorkflowSkills.some((selectedSkill) => selectedSkill.id === skill.id));
38
+ const previews = [];
39
+ const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, plannedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
40
+ const alreadyPreviewedPaths = new Set();
41
+ for (const skill of missingImpliedWorkflowSkills) {
42
+ for (const preview of getNewExpectedTargetPreviews({ rootPath, manifest, state, expectedTargets, workflowSkill: skill })) {
43
+ previews.push(preview);
44
+ alreadyPreviewedPaths.add(preview.relativePath);
45
+ }
46
+ }
47
+ previews.push(...Object.entries(state.managedFiles).map(([relativePath, managedFile]) => getManagedFileSyncPreview({
36
48
  rootPath,
37
49
  manifest,
38
50
  relativePath,
@@ -40,46 +52,65 @@ export function getSyncPreviews({ rootPath, state, manifest }) {
40
52
  selectedLanguageSkills,
41
53
  selectedFrameworkSkills,
42
54
  selectedArchitectureSkill,
43
- selectedWorkflowSkills,
55
+ selectedWorkflowSkills: plannedWorkflowSkills,
44
56
  selectedDatabaseSkills,
45
57
  selectedMcpServers,
46
- }));
47
- const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
58
+ })));
48
59
  for (const target of expectedTargets) {
49
60
  const relativePath = path.relative(rootPath, target.targetPath);
50
- if (state.managedFiles[relativePath]) {
51
- continue;
52
- }
53
- const template = manifest.templates[target.templateRelativePath];
54
- if (!template) {
55
- previews.push({
56
- relativePath,
57
- managedFile: {
58
- template: target.templateRelativePath,
59
- templateVersion: 'unknown',
60
- sha256: '',
61
- },
62
- status: 'unknown-template',
63
- changes: [],
64
- hasLocalFile: fs.existsSync(target.targetPath),
65
- });
61
+ if (state.managedFiles[relativePath] || alreadyPreviewedPaths.has(relativePath)) {
66
62
  continue;
67
63
  }
68
- previews.push({
64
+ previews.push(getNewExpectedTargetPreview({ rootPath, manifest, target }));
65
+ }
66
+ return previews;
67
+ }
68
+ function getNewExpectedTargetPreviews({ rootPath, manifest, state, expectedTargets, workflowSkill, }) {
69
+ const workflowSkillTargetPaths = new Set(Object.values(workflowSkill.targetRelativePathsByAgent));
70
+ return expectedTargets
71
+ .filter((target) => workflowSkillTargetPaths.has(path.relative(rootPath, target.targetPath)))
72
+ .filter((target) => !state.managedFiles[path.relative(rootPath, target.targetPath)])
73
+ .map((target) => ({
74
+ ...getNewExpectedTargetPreview({ rootPath, manifest, target }),
75
+ impliedWorkflowSkillId: workflowSkill.id,
76
+ }));
77
+ }
78
+ function getNewExpectedTargetPreview({ rootPath, manifest, target }) {
79
+ const relativePath = path.relative(rootPath, target.targetPath);
80
+ const template = manifest.templates[target.templateRelativePath];
81
+ if (!template) {
82
+ return {
69
83
  relativePath,
70
84
  managedFile: {
71
85
  template: target.templateRelativePath,
72
- templateVersion: template.version,
86
+ templateVersion: 'unknown',
73
87
  sha256: '',
74
- status: 'managed',
75
88
  },
76
- status: 'new-template',
77
- currentTemplateVersion: template.version,
78
- changes: template.changes,
89
+ status: 'unknown-template',
90
+ changes: [],
79
91
  hasLocalFile: fs.existsSync(target.targetPath),
80
- });
92
+ };
81
93
  }
82
- return previews;
94
+ return {
95
+ relativePath,
96
+ managedFile: {
97
+ template: target.templateRelativePath,
98
+ templateVersion: template.version,
99
+ sha256: '',
100
+ status: 'managed',
101
+ },
102
+ status: 'new-template',
103
+ currentTemplateVersion: template.version,
104
+ changes: template.changes,
105
+ hasLocalFile: fs.existsSync(target.targetPath),
106
+ };
107
+ }
108
+ function mergeWorkflowSkills(...skillGroups) {
109
+ const skillsById = new Map();
110
+ for (const skill of skillGroups.flat()) {
111
+ skillsById.set(skill.id, skill);
112
+ }
113
+ return [...skillsById.values()];
83
114
  }
84
115
  function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers, }) {
85
116
  if (managedFile.status === 'unmanaged') {