@juancr11/sibu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/bin/entrypoints/cli/command.js +1 -0
- package/bin/entrypoints/cli/create-program.js +33 -0
- package/bin/entrypoints/cli/execute-command.js +28 -0
- package/bin/entrypoints/cli/main.js +5 -0
- package/bin/features/doctor-project/command.js +1 -0
- package/bin/features/doctor-project/handler.js +194 -0
- package/bin/features/init-project/command.js +1 -0
- package/bin/features/init-project/handler.js +63 -0
- package/bin/features/list-skills/command.js +1 -0
- package/bin/features/list-skills/handler.js +55 -0
- package/bin/features/stop-managing-file/command.js +1 -0
- package/bin/features/stop-managing-file/handler.js +213 -0
- package/bin/features/sync-project/action-prompt.js +65 -0
- package/bin/features/sync-project/apply-action.js +81 -0
- package/bin/features/sync-project/command.js +1 -0
- package/bin/features/sync-project/handler.js +77 -0
- package/bin/features/sync-project/log-preview.js +62 -0
- package/bin/features/sync-project/preview.js +1 -0
- package/bin/features/use-skill/command.js +1 -0
- package/bin/features/use-skill/handler.js +197 -0
- package/bin/shared/catalog.js +199 -0
- package/bin/shared/hash.js +11 -0
- package/bin/shared/npm-version.js +178 -0
- package/bin/shared/object.js +3 -0
- package/bin/shared/paths.js +41 -0
- package/bin/shared/prompts.js +205 -0
- package/bin/shared/state.js +76 -0
- package/bin/shared/sync-preview.js +166 -0
- package/bin/shared/templates.js +60 -0
- package/bin/shared/types.js +1 -0
- package/bin/shared/workflow-mutation-readiness.js +30 -0
- package/bin/shared/workflow-targets.js +119 -0
- package/bin/sibu.js +6 -0
- package/package.json +68 -0
- package/templates/.codex/config.toml +1 -0
- package/templates/AGENTS.md +60 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/GEMINI.md +5 -0
- package/templates/manifest.json +129 -0
- package/templates/skills/ai-implementation-plan-executor/SKILL.md +138 -0
- package/templates/skills/ai-implementation-planner/SKILL.md +213 -0
- package/templates/skills/architecture/command-pattern/SKILL.md +77 -0
- package/templates/skills/architecture/ddd-hexagonal/SKILL.md +212 -0
- package/templates/skills/clean-code/SKILL.md +109 -0
- package/templates/skills/feature-brief-writer/SKILL.md +219 -0
- package/templates/skills/golang/SKILL.md +82 -0
- package/templates/skills/nextjs/SKILL.md +94 -0
- package/templates/skills/product-vision-writer/SKILL.md +128 -0
- package/templates/skills/react/SKILL.md +75 -0
- package/templates/skills/scrum-master-planner/SKILL.md +191 -0
- package/templates/skills/technical-design-writer/SKILL.md +109 -0
- package/templates/skills/typescript/SKILL.md +111 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log } from '@clack/prompts';
|
|
4
|
+
import { STATE_RELATIVE_PATH, resolveSelectableSkillById } from '../../shared/catalog.js';
|
|
5
|
+
import { sha256 } from '../../shared/hash.js';
|
|
6
|
+
import { getProjectContext } from '../../shared/paths.js';
|
|
7
|
+
import { renderTemplateForSync } from '../../shared/templates.js';
|
|
8
|
+
import { getWorkflowMutationReadiness } from '../../shared/workflow-mutation-readiness.js';
|
|
9
|
+
import { getSelectedAgentsFromState, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../../shared/workflow-targets.js';
|
|
10
|
+
export async function handleUseSkill(command) {
|
|
11
|
+
const { rootPath, statePath } = getProjectContext();
|
|
12
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
13
|
+
if (!readiness.ok) {
|
|
14
|
+
log.error(readiness.message);
|
|
15
|
+
log.info(readiness.hint);
|
|
16
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
17
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
18
|
+
}
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const selectionResult = getNextSkillSelection(readiness.state, command.skillName);
|
|
23
|
+
switch (selectionResult.status) {
|
|
24
|
+
case 'noop':
|
|
25
|
+
log.success(selectionResult.message);
|
|
26
|
+
log.info('No files changed.');
|
|
27
|
+
return;
|
|
28
|
+
case 'blocked':
|
|
29
|
+
log.error(selectionResult.message);
|
|
30
|
+
if (selectionResult.hint) {
|
|
31
|
+
log.info(selectionResult.hint);
|
|
32
|
+
}
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
case 'selected':
|
|
36
|
+
applySelectedSkill({ rootPath, statePath, state: readiness.state, selectionResult });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function getNextSkillSelection(state, skillName) {
|
|
41
|
+
const resolution = resolveSelectableSkillById(skillName);
|
|
42
|
+
if (!resolution.ok) {
|
|
43
|
+
return { status: 'blocked', message: resolution.message };
|
|
44
|
+
}
|
|
45
|
+
const selectedLanguageSkills = [...(state.selectedLanguageSkills ?? [])];
|
|
46
|
+
const selectedFrameworkSkills = [...(state.selectedFrameworkSkills ?? [])];
|
|
47
|
+
switch (resolution.resolved.kind) {
|
|
48
|
+
case 'language':
|
|
49
|
+
if (selectedLanguageSkills.includes(resolution.resolved.skill.id)) {
|
|
50
|
+
return { status: 'noop', message: `${resolution.resolved.skill.name} is already selected.` };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
status: 'selected',
|
|
54
|
+
skillName: resolution.resolved.skill.name,
|
|
55
|
+
selection: {
|
|
56
|
+
selectedLanguageSkills: [...selectedLanguageSkills, resolution.resolved.skill.id].map(getLanguageSkillById),
|
|
57
|
+
selectedFrameworkSkills: selectedFrameworkSkills.map(getFrameworkSkillById),
|
|
58
|
+
selectedArchitectureSkill: getArchitectureSkillById(state.selectedArchitectureSkill),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
case 'framework':
|
|
62
|
+
if (selectedFrameworkSkills.includes(resolution.resolved.skill.id)) {
|
|
63
|
+
return { status: 'noop', message: `${resolution.resolved.skill.name} is already selected.` };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
status: 'selected',
|
|
67
|
+
skillName: resolution.resolved.skill.name,
|
|
68
|
+
selection: {
|
|
69
|
+
selectedLanguageSkills: selectedLanguageSkills.map(getLanguageSkillById),
|
|
70
|
+
selectedFrameworkSkills: [...selectedFrameworkSkills, resolution.resolved.skill.id].map(getFrameworkSkillById),
|
|
71
|
+
selectedArchitectureSkill: getArchitectureSkillById(state.selectedArchitectureSkill),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
case 'architecture':
|
|
75
|
+
if (state.selectedArchitectureSkill === resolution.resolved.skill.id) {
|
|
76
|
+
return { status: 'noop', message: `${resolution.resolved.skill.name} is already selected.` };
|
|
77
|
+
}
|
|
78
|
+
if (state.selectedArchitectureSkill) {
|
|
79
|
+
return {
|
|
80
|
+
status: 'blocked',
|
|
81
|
+
message: `Cannot select ${resolution.resolved.skill.name} because another architecture skill is already selected.`,
|
|
82
|
+
hint: 'Architecture skill replacement is not supported yet. Keep the existing architecture skill or stop managing it first.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
status: 'selected',
|
|
87
|
+
skillName: resolution.resolved.skill.name,
|
|
88
|
+
selection: {
|
|
89
|
+
selectedLanguageSkills: selectedLanguageSkills.map(getLanguageSkillById),
|
|
90
|
+
selectedFrameworkSkills: selectedFrameworkSkills.map(getFrameworkSkillById),
|
|
91
|
+
selectedArchitectureSkill: resolution.resolved.skill,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function applySelectedSkill({ rootPath, statePath, state, selectionResult, }) {
|
|
97
|
+
const plan = buildSkillApplicationPlan({ rootPath, state, selectionResult });
|
|
98
|
+
const preflightError = getSkillApplicationPreflightError({ rootPath, state, plan });
|
|
99
|
+
if (preflightError) {
|
|
100
|
+
log.error(preflightError);
|
|
101
|
+
log.info('Run `sibu sync` to review workflow state before selecting a skill.');
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const agentsContents = renderTemplateForSync({
|
|
106
|
+
templateRelativePath: plan.agentsTarget.templateRelativePath,
|
|
107
|
+
currentPath: plan.agentsTarget.targetPath,
|
|
108
|
+
selectedLanguageSkills: selectionResult.selection.selectedLanguageSkills,
|
|
109
|
+
selectedFrameworkSkills: selectionResult.selection.selectedFrameworkSkills,
|
|
110
|
+
selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
|
|
111
|
+
});
|
|
112
|
+
const [newSkillFile] = renderMissingWorkflowFiles({
|
|
113
|
+
missingTargets: [plan.newSkillTarget],
|
|
114
|
+
selectedLanguageSkills: selectionResult.selection.selectedLanguageSkills,
|
|
115
|
+
selectedFrameworkSkills: selectionResult.selection.selectedFrameworkSkills,
|
|
116
|
+
selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
|
|
117
|
+
});
|
|
118
|
+
fs.mkdirSync(path.dirname(newSkillFile.targetPath), { recursive: true });
|
|
119
|
+
fs.writeFileSync(newSkillFile.targetPath, newSkillFile.contents, { encoding: 'utf8', flag: 'wx' });
|
|
120
|
+
log.success(`Created ${newSkillFile.label}`);
|
|
121
|
+
fs.writeFileSync(plan.agentsTarget.targetPath, agentsContents, 'utf8');
|
|
122
|
+
log.success('Updated AGENTS.md skill routing');
|
|
123
|
+
writeSibuState({
|
|
124
|
+
rootPath,
|
|
125
|
+
statePath,
|
|
126
|
+
selectedAgents: plan.selectedAgents,
|
|
127
|
+
selectedLanguageSkills: selectionResult.selection.selectedLanguageSkills,
|
|
128
|
+
selectedFrameworkSkills: selectionResult.selection.selectedFrameworkSkills,
|
|
129
|
+
selectedArchitectureSkill: selectionResult.selection.selectedArchitectureSkill,
|
|
130
|
+
targets: plan.targets,
|
|
131
|
+
});
|
|
132
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
133
|
+
log.success(`Added ${selectionResult.skillName}.`);
|
|
134
|
+
}
|
|
135
|
+
function buildSkillApplicationPlan({ rootPath, state, selectionResult, }) {
|
|
136
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
137
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, getSelectedLanguageSkillsFromState(state), getSelectedFrameworkSkillsFromState(state), getArchitectureSkillById(state.selectedArchitectureSkill));
|
|
138
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectionResult.selection.selectedLanguageSkills, selectionResult.selection.selectedFrameworkSkills, selectionResult.selection.selectedArchitectureSkill);
|
|
139
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
140
|
+
const newSkillTarget = targets.find((target) => !previousTargetPaths.has(target.targetPath));
|
|
141
|
+
const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
|
|
142
|
+
if (!newSkillTarget) {
|
|
143
|
+
throw new Error(`No new workflow target found for ${selectionResult.skillName}.`);
|
|
144
|
+
}
|
|
145
|
+
if (!agentsTarget) {
|
|
146
|
+
throw new Error('AGENTS.md target is missing from workflow targets.');
|
|
147
|
+
}
|
|
148
|
+
return { agentsTarget, newSkillTarget, targets, selectedAgents };
|
|
149
|
+
}
|
|
150
|
+
function getSkillApplicationPreflightError({ rootPath, state, plan }) {
|
|
151
|
+
if (fs.existsSync(plan.newSkillTarget.targetPath)) {
|
|
152
|
+
return `${path.relative(rootPath, plan.newSkillTarget.targetPath)} already exists but is not recorded for this selection.`;
|
|
153
|
+
}
|
|
154
|
+
const agentsRelativePath = path.relative(rootPath, plan.agentsTarget.targetPath);
|
|
155
|
+
const agentsManagedFile = state.managedFiles[agentsRelativePath];
|
|
156
|
+
if (!agentsManagedFile) {
|
|
157
|
+
return 'AGENTS.md is not recorded in `.sibu/state.json`.';
|
|
158
|
+
}
|
|
159
|
+
if (!fs.existsSync(plan.agentsTarget.targetPath)) {
|
|
160
|
+
return 'AGENTS.md is missing.';
|
|
161
|
+
}
|
|
162
|
+
const agentsCurrentHash = sha256(fs.readFileSync(plan.agentsTarget.targetPath, 'utf8'));
|
|
163
|
+
if (agentsCurrentHash !== agentsManagedFile.sha256) {
|
|
164
|
+
return 'AGENTS.md has changed since Sibu last recorded it.';
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function getSelectedLanguageSkillsFromState(state) {
|
|
169
|
+
return (state.selectedLanguageSkills ?? []).map(getLanguageSkillById);
|
|
170
|
+
}
|
|
171
|
+
function getSelectedFrameworkSkillsFromState(state) {
|
|
172
|
+
return (state.selectedFrameworkSkills ?? []).map(getFrameworkSkillById);
|
|
173
|
+
}
|
|
174
|
+
function getLanguageSkillById(skillId) {
|
|
175
|
+
const resolution = resolveSelectableSkillById(skillId);
|
|
176
|
+
if (!resolution.ok || resolution.resolved.kind !== 'language') {
|
|
177
|
+
throw new Error(`Unsupported language skill in state: ${skillId}`);
|
|
178
|
+
}
|
|
179
|
+
return resolution.resolved.skill;
|
|
180
|
+
}
|
|
181
|
+
function getFrameworkSkillById(skillId) {
|
|
182
|
+
const resolution = resolveSelectableSkillById(skillId);
|
|
183
|
+
if (!resolution.ok || resolution.resolved.kind !== 'framework') {
|
|
184
|
+
throw new Error(`Unsupported framework skill in state: ${skillId}`);
|
|
185
|
+
}
|
|
186
|
+
return resolution.resolved.skill;
|
|
187
|
+
}
|
|
188
|
+
function getArchitectureSkillById(skillId) {
|
|
189
|
+
if (!skillId) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const resolution = resolveSelectableSkillById(skillId);
|
|
193
|
+
if (!resolution.ok || resolution.resolved.kind !== 'architecture') {
|
|
194
|
+
throw new Error(`Unsupported architecture skill in state: ${skillId}`);
|
|
195
|
+
}
|
|
196
|
+
return resolution.resolved.skill;
|
|
197
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export const SIBU_PACKAGE_NAME = '@juancr11/sibu';
|
|
2
|
+
export const SIBU_VERSION = '0.1.0';
|
|
3
|
+
export const STATE_RELATIVE_PATH = '.sibu/state.json';
|
|
4
|
+
export const NPM_VERSION_LOOKUP_MODE_ENV = 'SIBU_NPM_LOOKUP_MODE';
|
|
5
|
+
export const NPM_VERSION_OVERRIDE_ENV = 'SIBU_NPM_LATEST_VERSION';
|
|
6
|
+
export const SIBU_CACHE_HOME_ENV = 'SIBU_CACHE_HOME';
|
|
7
|
+
export const SUPPORTED_NPM_LOOKUP_MODES = ['live', 'offline'];
|
|
8
|
+
export const MANDATORY_SKILLS = [
|
|
9
|
+
{
|
|
10
|
+
templateRelativePath: 'skills/clean-code/SKILL.md',
|
|
11
|
+
targetRelativePathsByAgent: {
|
|
12
|
+
codex: '.agents/skills/clean-code/SKILL.md',
|
|
13
|
+
gemini: '.agents/skills/clean-code/SKILL.md',
|
|
14
|
+
claude: '.agents/skills/clean-code/SKILL.md',
|
|
15
|
+
windsurf: '.agents/skills/clean-code/SKILL.md',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
templateRelativePath: 'skills/product-vision-writer/SKILL.md',
|
|
20
|
+
targetRelativePathsByAgent: {
|
|
21
|
+
codex: '.agents/skills/product-vision-writer/SKILL.md',
|
|
22
|
+
gemini: '.agents/skills/product-vision-writer/SKILL.md',
|
|
23
|
+
claude: '.agents/skills/product-vision-writer/SKILL.md',
|
|
24
|
+
windsurf: '.agents/skills/product-vision-writer/SKILL.md',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
templateRelativePath: 'skills/feature-brief-writer/SKILL.md',
|
|
29
|
+
targetRelativePathsByAgent: {
|
|
30
|
+
codex: '.agents/skills/feature-brief-writer/SKILL.md',
|
|
31
|
+
gemini: '.agents/skills/feature-brief-writer/SKILL.md',
|
|
32
|
+
claude: '.agents/skills/feature-brief-writer/SKILL.md',
|
|
33
|
+
windsurf: '.agents/skills/feature-brief-writer/SKILL.md',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
templateRelativePath: 'skills/technical-design-writer/SKILL.md',
|
|
38
|
+
targetRelativePathsByAgent: {
|
|
39
|
+
codex: '.agents/skills/technical-design-writer/SKILL.md',
|
|
40
|
+
gemini: '.agents/skills/technical-design-writer/SKILL.md',
|
|
41
|
+
claude: '.agents/skills/technical-design-writer/SKILL.md',
|
|
42
|
+
windsurf: '.agents/skills/technical-design-writer/SKILL.md',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
templateRelativePath: 'skills/scrum-master-planner/SKILL.md',
|
|
47
|
+
targetRelativePathsByAgent: {
|
|
48
|
+
codex: '.agents/skills/scrum-master-planner/SKILL.md',
|
|
49
|
+
gemini: '.agents/skills/scrum-master-planner/SKILL.md',
|
|
50
|
+
claude: '.agents/skills/scrum-master-planner/SKILL.md',
|
|
51
|
+
windsurf: '.agents/skills/scrum-master-planner/SKILL.md',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
templateRelativePath: 'skills/ai-implementation-planner/SKILL.md',
|
|
56
|
+
targetRelativePathsByAgent: {
|
|
57
|
+
codex: '.agents/skills/ai-implementation-planner/SKILL.md',
|
|
58
|
+
gemini: '.agents/skills/ai-implementation-planner/SKILL.md',
|
|
59
|
+
claude: '.agents/skills/ai-implementation-planner/SKILL.md',
|
|
60
|
+
windsurf: '.agents/skills/ai-implementation-planner/SKILL.md',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
templateRelativePath: 'skills/ai-implementation-plan-executor/SKILL.md',
|
|
65
|
+
targetRelativePathsByAgent: {
|
|
66
|
+
codex: '.agents/skills/ai-implementation-plan-executor/SKILL.md',
|
|
67
|
+
gemini: '.agents/skills/ai-implementation-plan-executor/SKILL.md',
|
|
68
|
+
claude: '.agents/skills/ai-implementation-plan-executor/SKILL.md',
|
|
69
|
+
windsurf: '.agents/skills/ai-implementation-plan-executor/SKILL.md',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
export const SELECTABLE_LANGUAGE_SKILLS = [
|
|
74
|
+
{
|
|
75
|
+
id: 'typescript',
|
|
76
|
+
name: 'TypeScript',
|
|
77
|
+
description: 'Install practical guidance for writing and modifying .ts and .tsx files',
|
|
78
|
+
routingInstruction: 'For any task that changes `.ts` or `.tsx` files, also use `typescript`.',
|
|
79
|
+
templateRelativePath: 'skills/typescript/SKILL.md',
|
|
80
|
+
targetRelativePathsByAgent: {
|
|
81
|
+
codex: '.agents/skills/typescript/SKILL.md',
|
|
82
|
+
gemini: '.agents/skills/typescript/SKILL.md',
|
|
83
|
+
claude: '.agents/skills/typescript/SKILL.md',
|
|
84
|
+
windsurf: '.agents/skills/typescript/SKILL.md',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'golang',
|
|
89
|
+
name: 'Go',
|
|
90
|
+
description: 'Install practical guidance for writing and modifying .go files',
|
|
91
|
+
routingInstruction: 'For any task that changes `.go` files, also use `golang`.',
|
|
92
|
+
templateRelativePath: 'skills/golang/SKILL.md',
|
|
93
|
+
targetRelativePathsByAgent: {
|
|
94
|
+
codex: '.agents/skills/golang/SKILL.md',
|
|
95
|
+
gemini: '.agents/skills/golang/SKILL.md',
|
|
96
|
+
claude: '.agents/skills/golang/SKILL.md',
|
|
97
|
+
windsurf: '.agents/skills/golang/SKILL.md',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
export const SELECTABLE_FRAMEWORK_SKILLS = [
|
|
102
|
+
{
|
|
103
|
+
id: 'react',
|
|
104
|
+
name: 'React',
|
|
105
|
+
description: 'Install guidance for React components, props, state ownership, and component boundaries',
|
|
106
|
+
routingInstruction: 'For tasks that create or change React components, component responsibility, props, state ownership, or presentational vs data-owning boundaries, use `react`.',
|
|
107
|
+
templateRelativePath: 'skills/react/SKILL.md',
|
|
108
|
+
targetRelativePathsByAgent: {
|
|
109
|
+
codex: '.agents/skills/react/SKILL.md',
|
|
110
|
+
gemini: '.agents/skills/react/SKILL.md',
|
|
111
|
+
claude: '.agents/skills/react/SKILL.md',
|
|
112
|
+
windsurf: '.agents/skills/react/SKILL.md',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'nextjs',
|
|
117
|
+
name: 'Next.js',
|
|
118
|
+
description: 'Install guidance for Next.js App Router and framework-specific files',
|
|
119
|
+
routingInstruction: 'For tasks that change Next.js App Router or framework-specific files, including `src/app/**`, pages, layouts, route handlers, loading/error/not-found files, metadata, or Server/Client Component boundaries, use `nextjs`.',
|
|
120
|
+
templateRelativePath: 'skills/nextjs/SKILL.md',
|
|
121
|
+
targetRelativePathsByAgent: {
|
|
122
|
+
codex: '.agents/skills/nextjs/SKILL.md',
|
|
123
|
+
gemini: '.agents/skills/nextjs/SKILL.md',
|
|
124
|
+
claude: '.agents/skills/nextjs/SKILL.md',
|
|
125
|
+
windsurf: '.agents/skills/nextjs/SKILL.md',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
export const SELECTABLE_ARCHITECTURE_SKILLS = [
|
|
130
|
+
{
|
|
131
|
+
id: 'ddd-hexagonal',
|
|
132
|
+
name: 'DDD + Hexagonal Architecture',
|
|
133
|
+
description: 'Install back-end architecture guidance for DDD, ports/adapters, and inward dependencies',
|
|
134
|
+
routingInstruction: 'For any back-end work, use `ddd-hexagonal`. This includes new features, refactors, bug fixes, persistence, external integrations, application/service boundaries, domain modeling, and architectural tradeoffs.',
|
|
135
|
+
templateRelativePath: 'skills/architecture/ddd-hexagonal/SKILL.md',
|
|
136
|
+
targetRelativePathsByAgent: {
|
|
137
|
+
codex: '.agents/skills/ddd-hexagonal/SKILL.md',
|
|
138
|
+
gemini: '.agents/skills/ddd-hexagonal/SKILL.md',
|
|
139
|
+
claude: '.agents/skills/ddd-hexagonal/SKILL.md',
|
|
140
|
+
windsurf: '.agents/skills/ddd-hexagonal/SKILL.md',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'command-pattern',
|
|
145
|
+
name: 'Command Pattern',
|
|
146
|
+
description: 'Install architecture guidance for structuring executable operations as commands and handlers',
|
|
147
|
+
routingInstruction: 'For work that structures actions, workflows, command handlers, operation dispatch, request processing, or executable tasks, use `command-pattern`.',
|
|
148
|
+
templateRelativePath: 'skills/architecture/command-pattern/SKILL.md',
|
|
149
|
+
targetRelativePathsByAgent: {
|
|
150
|
+
codex: '.agents/skills/command-pattern/SKILL.md',
|
|
151
|
+
gemini: '.agents/skills/command-pattern/SKILL.md',
|
|
152
|
+
claude: '.agents/skills/command-pattern/SKILL.md',
|
|
153
|
+
windsurf: '.agents/skills/command-pattern/SKILL.md',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
export const SUPPORTED_AGENTS = [
|
|
158
|
+
{
|
|
159
|
+
id: 'codex',
|
|
160
|
+
name: 'Codex',
|
|
161
|
+
description: 'Create .codex/config.toml pointing Codex to AGENTS.md',
|
|
162
|
+
targetRelativePath: '.codex/config.toml',
|
|
163
|
+
templateRelativePath: '.codex/config.toml',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'gemini',
|
|
167
|
+
name: 'Gemini',
|
|
168
|
+
description: 'Create GEMINI.md that delegates to AGENTS.md',
|
|
169
|
+
targetRelativePath: 'GEMINI.md',
|
|
170
|
+
templateRelativePath: 'GEMINI.md',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'claude',
|
|
174
|
+
name: 'Claude',
|
|
175
|
+
description: 'Create CLAUDE.md that delegates to AGENTS.md',
|
|
176
|
+
targetRelativePath: 'CLAUDE.md',
|
|
177
|
+
templateRelativePath: 'CLAUDE.md',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'windsurf',
|
|
181
|
+
name: 'Windsurf',
|
|
182
|
+
description: 'Use root AGENTS.md and shared .agents/skills/ discovery',
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
export function resolveSelectableSkillById(skillId) {
|
|
186
|
+
const languageSkill = SELECTABLE_LANGUAGE_SKILLS.find((skill) => skill.id === skillId);
|
|
187
|
+
if (languageSkill) {
|
|
188
|
+
return { ok: true, resolved: { kind: 'language', skill: languageSkill } };
|
|
189
|
+
}
|
|
190
|
+
const frameworkSkill = SELECTABLE_FRAMEWORK_SKILLS.find((skill) => skill.id === skillId);
|
|
191
|
+
if (frameworkSkill) {
|
|
192
|
+
return { ok: true, resolved: { kind: 'framework', skill: frameworkSkill } };
|
|
193
|
+
}
|
|
194
|
+
const architectureSkill = SELECTABLE_ARCHITECTURE_SKILLS.find((skill) => skill.id === skillId);
|
|
195
|
+
if (architectureSkill) {
|
|
196
|
+
return { ok: true, resolved: { kind: 'architecture', skill: architectureSkill } };
|
|
197
|
+
}
|
|
198
|
+
return { ok: false, message: `Unknown skill \`${skillId}\`. Run \`sibu skills list\` to see available skills.` };
|
|
199
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
export function sha256(contents) {
|
|
4
|
+
return crypto.createHash('sha256').update(contents).digest('hex');
|
|
5
|
+
}
|
|
6
|
+
export function readFileHashIfPresent(filePath) {
|
|
7
|
+
if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
return sha256(fs.readFileSync(filePath, 'utf8'));
|
|
11
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { NPM_VERSION_LOOKUP_MODE_ENV, NPM_VERSION_OVERRIDE_ENV, SIBU_PACKAGE_NAME, SIBU_VERSION, SUPPORTED_NPM_LOOKUP_MODES } from './catalog.js';
|
|
4
|
+
import { getNpmVersionCachePath } from './paths.js';
|
|
5
|
+
const NPM_VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
6
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
7
|
+
export async function checkForLatestSibuVersion(options = {}) {
|
|
8
|
+
const now = options.now ?? new Date();
|
|
9
|
+
const overrideResult = readOverrideResult(now);
|
|
10
|
+
if (overrideResult) {
|
|
11
|
+
return overrideResult;
|
|
12
|
+
}
|
|
13
|
+
const cachedRecord = readCachedNpmVersionResult();
|
|
14
|
+
if (cachedRecord.ok && isFreshCacheRecord(cachedRecord.record, now)) {
|
|
15
|
+
return {
|
|
16
|
+
...cachedRecord.record,
|
|
17
|
+
source: 'cache',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const liveOutcome = await fetchLatestVersionOutcome({
|
|
21
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
22
|
+
now,
|
|
23
|
+
});
|
|
24
|
+
writeCachedNpmVersionResult(liveOutcome);
|
|
25
|
+
return liveOutcome;
|
|
26
|
+
}
|
|
27
|
+
async function fetchLatestVersionOutcome({ fetchImpl, now }) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetchImpl(`${NPM_REGISTRY_URL}/${encodeURIComponent(SIBU_PACKAGE_NAME)}/latest`, {
|
|
30
|
+
headers: { accept: 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
return buildUnavailableOutcome({ checkedAt: now.toISOString(), reason: 'network-error' });
|
|
34
|
+
}
|
|
35
|
+
const payload = (await response.json());
|
|
36
|
+
const latestVersion = getLatestVersionFromPayload(payload);
|
|
37
|
+
if (!latestVersion) {
|
|
38
|
+
return buildUnavailableOutcome({ checkedAt: now.toISOString(), reason: 'invalid-response' });
|
|
39
|
+
}
|
|
40
|
+
return buildVersionedOutcome({
|
|
41
|
+
checkedAt: now.toISOString(),
|
|
42
|
+
latestVersion,
|
|
43
|
+
source: 'live',
|
|
44
|
+
status: compareVersions(latestVersion, SIBU_VERSION) > 0 ? 'update-available' : 'up-to-date',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return buildUnavailableOutcome({ checkedAt: now.toISOString(), reason: 'network-error' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readOverrideResult(now) {
|
|
52
|
+
const overrideMode = readLookupModeOverride();
|
|
53
|
+
if (overrideMode === 'offline') {
|
|
54
|
+
return buildUnavailableOutcome({
|
|
55
|
+
checkedAt: now.toISOString(),
|
|
56
|
+
reason: 'override',
|
|
57
|
+
source: 'override',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const overrideVersion = process.env[NPM_VERSION_OVERRIDE_ENV]?.trim();
|
|
61
|
+
if (!overrideVersion) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return buildVersionedOutcome({
|
|
65
|
+
checkedAt: now.toISOString(),
|
|
66
|
+
latestVersion: overrideVersion,
|
|
67
|
+
source: 'override',
|
|
68
|
+
status: compareVersions(overrideVersion, SIBU_VERSION) > 0 ? 'update-available' : 'up-to-date',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function readLookupModeOverride() {
|
|
72
|
+
const value = process.env[NPM_VERSION_LOOKUP_MODE_ENV]?.trim().toLowerCase();
|
|
73
|
+
if (!value) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return SUPPORTED_NPM_LOOKUP_MODES.find((mode) => mode === value);
|
|
77
|
+
}
|
|
78
|
+
function readCachedNpmVersionResult() {
|
|
79
|
+
const cachePath = getNpmVersionCachePath();
|
|
80
|
+
if (!fs.existsSync(cachePath)) {
|
|
81
|
+
return { ok: false, reason: 'missing' };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const payload = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
85
|
+
if (!isNpmVersionCacheRecord(payload)) {
|
|
86
|
+
return { ok: false, reason: 'invalid' };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, record: payload };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { ok: false, reason: 'invalid' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function writeCachedNpmVersionResult(result) {
|
|
95
|
+
const cachePath = getNpmVersionCachePath();
|
|
96
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(cachePath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
function isFreshCacheRecord(record, now) {
|
|
100
|
+
const checkedAt = Date.parse(record.checkedAt);
|
|
101
|
+
if (Number.isNaN(checkedAt)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return now.getTime() - checkedAt < NPM_VERSION_CACHE_TTL_MS;
|
|
105
|
+
}
|
|
106
|
+
function getLatestVersionFromPayload(payload) {
|
|
107
|
+
if (!payload || typeof payload !== 'object') {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const version = payload.version;
|
|
111
|
+
return typeof version === 'string' && version.trim() ? version.trim() : undefined;
|
|
112
|
+
}
|
|
113
|
+
function buildVersionedOutcome({ checkedAt, latestVersion, source, status, }) {
|
|
114
|
+
return {
|
|
115
|
+
checkedAt,
|
|
116
|
+
currentVersion: SIBU_VERSION,
|
|
117
|
+
latestVersion,
|
|
118
|
+
packageName: SIBU_PACKAGE_NAME,
|
|
119
|
+
source,
|
|
120
|
+
status,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function buildUnavailableOutcome({ checkedAt, reason, source = 'live', }) {
|
|
124
|
+
return {
|
|
125
|
+
checkedAt,
|
|
126
|
+
packageName: SIBU_PACKAGE_NAME,
|
|
127
|
+
reason,
|
|
128
|
+
source,
|
|
129
|
+
status: 'unavailable',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function compareVersions(left, right) {
|
|
133
|
+
const leftParts = splitVersion(left);
|
|
134
|
+
const rightParts = splitVersion(right);
|
|
135
|
+
const maxLength = Math.max(leftParts.core.length, rightParts.core.length);
|
|
136
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
137
|
+
const leftValue = leftParts.core[index] ?? 0;
|
|
138
|
+
const rightValue = rightParts.core[index] ?? 0;
|
|
139
|
+
if (leftValue !== rightValue) {
|
|
140
|
+
return leftValue > rightValue ? 1 : -1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (leftParts.prerelease === rightParts.prerelease) {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
if (!leftParts.prerelease) {
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
if (!rightParts.prerelease) {
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
return leftParts.prerelease.localeCompare(rightParts.prerelease);
|
|
153
|
+
}
|
|
154
|
+
function splitVersion(version) {
|
|
155
|
+
const [core, prerelease] = version.trim().split('-', 2);
|
|
156
|
+
return {
|
|
157
|
+
core: core.split('.').map((part) => Number.parseInt(part, 10) || 0),
|
|
158
|
+
prerelease,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function isNpmVersionCacheRecord(value) {
|
|
162
|
+
if (!value || typeof value !== 'object') {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const record = value;
|
|
166
|
+
if (record.status === 'unavailable') {
|
|
167
|
+
return (typeof record.checkedAt === 'string' &&
|
|
168
|
+
typeof record.packageName === 'string' &&
|
|
169
|
+
typeof record.reason === 'string' &&
|
|
170
|
+
typeof record.source === 'string');
|
|
171
|
+
}
|
|
172
|
+
return ((record.status === 'up-to-date' || record.status === 'update-available') &&
|
|
173
|
+
typeof record.checkedAt === 'string' &&
|
|
174
|
+
typeof record.currentVersion === 'string' &&
|
|
175
|
+
typeof record.latestVersion === 'string' &&
|
|
176
|
+
typeof record.packageName === 'string' &&
|
|
177
|
+
typeof record.source === 'string');
|
|
178
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { SIBU_CACHE_HOME_ENV, STATE_RELATIVE_PATH } from './catalog.js';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export function getProjectContext() {
|
|
8
|
+
const rootPath = process.cwd();
|
|
9
|
+
return {
|
|
10
|
+
rootPath,
|
|
11
|
+
statePath: path.join(rootPath, STATE_RELATIVE_PATH),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function getTemplatesPath() {
|
|
15
|
+
return path.join(__dirname, '..', '..', 'templates');
|
|
16
|
+
}
|
|
17
|
+
export function resolveManagedFilePath(rootPath, file) {
|
|
18
|
+
const absolutePath = path.isAbsolute(file) ? path.resolve(file) : path.resolve(rootPath, file);
|
|
19
|
+
const relativePath = path.relative(rootPath, absolutePath);
|
|
20
|
+
if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
21
|
+
throw new Error(`Managed file path must be inside this project: ${file}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
absolutePath,
|
|
25
|
+
relativePath: relativePath.split(path.sep).join('/'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function getSideTemplatePath(rootPath, relativePath, templateVersion) {
|
|
29
|
+
const safeName = relativePath.replace(/[\\/]/g, '__');
|
|
30
|
+
return path.join(rootPath, '.sibu', 'sync', `${safeName}.template-v${templateVersion}`);
|
|
31
|
+
}
|
|
32
|
+
export function getSibuCacheRoot() {
|
|
33
|
+
const override = process.env[SIBU_CACHE_HOME_ENV]?.trim();
|
|
34
|
+
if (override) {
|
|
35
|
+
return path.resolve(override);
|
|
36
|
+
}
|
|
37
|
+
return path.join(os.homedir(), '.sibu');
|
|
38
|
+
}
|
|
39
|
+
export function getNpmVersionCachePath() {
|
|
40
|
+
return path.join(getSibuCacheRoot(), 'cache', 'npm-version.json');
|
|
41
|
+
}
|