@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,205 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cancel, isCancel, multiselect, select, text } from '@clack/prompts';
|
|
3
|
+
import gradient from 'gradient-string';
|
|
4
|
+
import { Box, Text, render, useApp } from 'ink';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SUPPORTED_AGENTS } from './catalog.js';
|
|
7
|
+
const NONE_OPTION_ID = 'none';
|
|
8
|
+
export async function renderIntro() {
|
|
9
|
+
console.log(gradient(['#39ff14', '#00e5ff', '#9b5de5']).multiline('⧖ S I B U ⧖'));
|
|
10
|
+
const app = render(_jsx(IntroPanel, {}));
|
|
11
|
+
await app.waitUntilExit();
|
|
12
|
+
}
|
|
13
|
+
function IntroPanel() {
|
|
14
|
+
const { exit } = useApp();
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const timer = setTimeout(exit, 650);
|
|
17
|
+
return () => clearTimeout(timer);
|
|
18
|
+
}, [exit]);
|
|
19
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyanBright", children: "Sibu is ready" }), _jsx(Text, { color: "greenBright", children: "Build. Test. Rewind. Improve." })] }));
|
|
20
|
+
}
|
|
21
|
+
export async function askForSupportedAgents() {
|
|
22
|
+
const selectedAgentIds = await multiselect({
|
|
23
|
+
message: 'Select the agents this project should support.',
|
|
24
|
+
required: true,
|
|
25
|
+
options: SUPPORTED_AGENTS.map((agent) => ({
|
|
26
|
+
value: agent.id,
|
|
27
|
+
label: agent.name,
|
|
28
|
+
hint: agent.description,
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
if (isCancel(selectedAgentIds)) {
|
|
32
|
+
cancel('Initialization cancelled.');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
return SUPPORTED_AGENTS.filter((agent) => selectedAgentIds.includes(agent.id));
|
|
36
|
+
}
|
|
37
|
+
export async function askForLanguageSkills() {
|
|
38
|
+
const selectedLanguageSkillIds = await multiselect({
|
|
39
|
+
message: 'Select the languages this project should support.',
|
|
40
|
+
required: false,
|
|
41
|
+
options: SELECTABLE_LANGUAGE_SKILLS.map((skill) => ({
|
|
42
|
+
value: skill.id,
|
|
43
|
+
label: skill.name,
|
|
44
|
+
hint: skill.description,
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
if (isCancel(selectedLanguageSkillIds)) {
|
|
48
|
+
cancel('Initialization cancelled.');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
return SELECTABLE_LANGUAGE_SKILLS.filter((skill) => selectedLanguageSkillIds.includes(skill.id));
|
|
52
|
+
}
|
|
53
|
+
export async function askForFrameworkSkills() {
|
|
54
|
+
return askForFrameworkSkillSelection({
|
|
55
|
+
message: 'Select the frameworks this project should support.',
|
|
56
|
+
cancelMessage: 'Initialization cancelled.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function askForFrameworkSkillSelection({ message, cancelMessage, }) {
|
|
60
|
+
const selectedFrameworkSkillIds = await multiselect({
|
|
61
|
+
message,
|
|
62
|
+
required: false,
|
|
63
|
+
options: [
|
|
64
|
+
{ value: NONE_OPTION_ID, label: 'None', hint: 'Do not install framework-specific guidance.' },
|
|
65
|
+
...SELECTABLE_FRAMEWORK_SKILLS.map((skill) => ({
|
|
66
|
+
value: skill.id,
|
|
67
|
+
label: skill.name,
|
|
68
|
+
hint: skill.description,
|
|
69
|
+
})),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
if (isCancel(selectedFrameworkSkillIds)) {
|
|
73
|
+
cancel(cancelMessage);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (selectedFrameworkSkillIds.includes(NONE_OPTION_ID)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return SELECTABLE_FRAMEWORK_SKILLS.filter((skill) => selectedFrameworkSkillIds.includes(skill.id));
|
|
80
|
+
}
|
|
81
|
+
export async function askForArchitectureSkill() {
|
|
82
|
+
const selectedArchitectureSkillId = await select({
|
|
83
|
+
message: 'Select an architecture style for this project.',
|
|
84
|
+
options: [
|
|
85
|
+
{ value: 'none', label: 'None', hint: 'Do not install opinionated architecture guidance.' },
|
|
86
|
+
...SELECTABLE_ARCHITECTURE_SKILLS.map((skill) => ({
|
|
87
|
+
value: skill.id,
|
|
88
|
+
label: skill.name,
|
|
89
|
+
hint: skill.description,
|
|
90
|
+
})),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
if (isCancel(selectedArchitectureSkillId)) {
|
|
94
|
+
cancel('Initialization cancelled.');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
if (selectedArchitectureSkillId === 'none') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return SELECTABLE_ARCHITECTURE_SKILLS.find((skill) => skill.id === selectedArchitectureSkillId);
|
|
101
|
+
}
|
|
102
|
+
export function shouldAskForNewLanguageSkills(state) {
|
|
103
|
+
return (state.selectedLanguageSkills?.length ?? 0) === 0;
|
|
104
|
+
}
|
|
105
|
+
export async function askForNewLanguageSkills(state) {
|
|
106
|
+
if (!shouldAskForNewLanguageSkills(state)) {
|
|
107
|
+
return { state, changedState: false };
|
|
108
|
+
}
|
|
109
|
+
const selectedLanguageSkillIds = new Set(state.selectedLanguageSkills ?? []);
|
|
110
|
+
const unselectedLanguageSkills = SELECTABLE_LANGUAGE_SKILLS.filter((skill) => !selectedLanguageSkillIds.has(skill.id));
|
|
111
|
+
if (unselectedLanguageSkills.length === 0) {
|
|
112
|
+
return { state, changedState: false };
|
|
113
|
+
}
|
|
114
|
+
const selectedNewLanguageSkillIds = await multiselect({
|
|
115
|
+
message: 'Select any new languages this project should support.',
|
|
116
|
+
required: false,
|
|
117
|
+
options: unselectedLanguageSkills.map((skill) => ({
|
|
118
|
+
value: skill.id,
|
|
119
|
+
label: skill.name,
|
|
120
|
+
hint: skill.description,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
if (isCancel(selectedNewLanguageSkillIds)) {
|
|
124
|
+
cancel('Sync cancelled.');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
for (const selectedSkillId of selectedNewLanguageSkillIds) {
|
|
128
|
+
selectedLanguageSkillIds.add(selectedSkillId);
|
|
129
|
+
}
|
|
130
|
+
if (selectedNewLanguageSkillIds.length === 0) {
|
|
131
|
+
return { state, changedState: false };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
state: {
|
|
135
|
+
...state,
|
|
136
|
+
selectedLanguageSkills: [...selectedLanguageSkillIds],
|
|
137
|
+
updatedAt: new Date().toISOString(),
|
|
138
|
+
},
|
|
139
|
+
changedState: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export async function askForMissingFrameworkSkills(state) {
|
|
143
|
+
if (state.selectedFrameworkSkills !== undefined) {
|
|
144
|
+
return { state, changedState: false };
|
|
145
|
+
}
|
|
146
|
+
const selectedFrameworkSkills = await askForFrameworkSkillSelection({
|
|
147
|
+
message: 'Select the frameworks this project should support.',
|
|
148
|
+
cancelMessage: 'Sync cancelled.',
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
state: {
|
|
152
|
+
...state,
|
|
153
|
+
selectedFrameworkSkills: selectedFrameworkSkills.map((skill) => skill.id),
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
},
|
|
156
|
+
changedState: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export async function askForNewArchitectureSkill(state) {
|
|
160
|
+
if (state.selectedArchitectureSkill) {
|
|
161
|
+
return { state, changedState: false };
|
|
162
|
+
}
|
|
163
|
+
const selectedArchitectureSkillId = await select({
|
|
164
|
+
message: 'Select an architecture style for this project.',
|
|
165
|
+
options: [
|
|
166
|
+
{ value: 'none', label: 'None', hint: 'Do not install opinionated architecture guidance.' },
|
|
167
|
+
...SELECTABLE_ARCHITECTURE_SKILLS.map((skill) => ({
|
|
168
|
+
value: skill.id,
|
|
169
|
+
label: skill.name,
|
|
170
|
+
hint: skill.description,
|
|
171
|
+
})),
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
if (isCancel(selectedArchitectureSkillId)) {
|
|
175
|
+
cancel('Sync cancelled.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
state: selectedArchitectureSkillId === 'none'
|
|
180
|
+
? state
|
|
181
|
+
: {
|
|
182
|
+
...state,
|
|
183
|
+
selectedArchitectureSkill: selectedArchitectureSkillId,
|
|
184
|
+
updatedAt: new Date().toISOString(),
|
|
185
|
+
},
|
|
186
|
+
changedState: selectedArchitectureSkillId !== 'none',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export async function askForProjectOverview() {
|
|
190
|
+
const overview = await text({
|
|
191
|
+
message: 'Tell me what this project is about.',
|
|
192
|
+
placeholder: 'A local-first notes app for software teams.',
|
|
193
|
+
validate(value) {
|
|
194
|
+
if (!value?.trim()) {
|
|
195
|
+
return 'Please enter a short project overview so I can create AGENTS.md.';
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (isCancel(overview)) {
|
|
201
|
+
cancel('Initialization cancelled.');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
return overview;
|
|
205
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { STATE_RELATIVE_PATH } from './catalog.js';
|
|
4
|
+
export function readStateForDoctor(statePath) {
|
|
5
|
+
if (!fs.existsSync(statePath)) {
|
|
6
|
+
return { ok: false, message: `${STATE_RELATIVE_PATH} is missing.` };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const parsedState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
10
|
+
if (!isSibuState(parsedState)) {
|
|
11
|
+
return { ok: false, message: `${STATE_RELATIVE_PATH} is not a valid Sibu state file.` };
|
|
12
|
+
}
|
|
13
|
+
return { ok: true, state: parsedState };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, message: `${STATE_RELATIVE_PATH} could not be parsed.` };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function readExistingState(statePath) {
|
|
20
|
+
if (!fs.existsSync(statePath)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function writeStateFile(statePath, state) {
|
|
31
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
export function cloneState(state) {
|
|
35
|
+
return JSON.parse(JSON.stringify(state));
|
|
36
|
+
}
|
|
37
|
+
export function hasReviewedTemplateVersion(managedFile, templateVersion) {
|
|
38
|
+
return managedFile.lastReviewedTemplateVersion === templateVersion;
|
|
39
|
+
}
|
|
40
|
+
function isSibuState(value) {
|
|
41
|
+
if (!value || typeof value !== 'object') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const state = value;
|
|
45
|
+
return (typeof state.sibuVersion === 'string' &&
|
|
46
|
+
typeof state.templateVersion === 'string' &&
|
|
47
|
+
typeof state.generatedAt === 'string' &&
|
|
48
|
+
typeof state.updatedAt === 'string' &&
|
|
49
|
+
Array.isArray(state.selectedAgents) &&
|
|
50
|
+
state.selectedAgents.every((agent) => typeof agent === 'string') &&
|
|
51
|
+
(state.selectedLanguageSkills === undefined ||
|
|
52
|
+
(Array.isArray(state.selectedLanguageSkills) && state.selectedLanguageSkills.every((skill) => typeof skill === 'string'))) &&
|
|
53
|
+
(state.reviewedLanguageSkills === undefined ||
|
|
54
|
+
(Array.isArray(state.reviewedLanguageSkills) && state.reviewedLanguageSkills.every((skill) => typeof skill === 'string'))) &&
|
|
55
|
+
(state.selectedFrameworkSkills === undefined ||
|
|
56
|
+
(Array.isArray(state.selectedFrameworkSkills) && state.selectedFrameworkSkills.every((skill) => typeof skill === 'string'))) &&
|
|
57
|
+
(state.selectedArchitectureSkill === undefined || typeof state.selectedArchitectureSkill === 'string') &&
|
|
58
|
+
(state.reviewedArchitectureSkills === undefined ||
|
|
59
|
+
(Array.isArray(state.reviewedArchitectureSkills) && state.reviewedArchitectureSkills.every((skill) => typeof skill === 'string'))) &&
|
|
60
|
+
!!state.managedFiles &&
|
|
61
|
+
typeof state.managedFiles === 'object' &&
|
|
62
|
+
Object.values(state.managedFiles).every(isManagedFileState));
|
|
63
|
+
}
|
|
64
|
+
function isManagedFileState(value) {
|
|
65
|
+
if (!value || typeof value !== 'object') {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const managedFile = value;
|
|
69
|
+
const validStatuses = ['managed', 'customized', 'unmanaged'];
|
|
70
|
+
return (typeof managedFile.template === 'string' &&
|
|
71
|
+
typeof managedFile.templateVersion === 'string' &&
|
|
72
|
+
typeof managedFile.sha256 === 'string' &&
|
|
73
|
+
(managedFile.status === undefined || validStatuses.includes(managedFile.status)) &&
|
|
74
|
+
(managedFile.lastReviewedTemplateVersion === undefined || typeof managedFile.lastReviewedTemplateVersion === 'string') &&
|
|
75
|
+
(managedFile.reason === undefined || typeof managedFile.reason === 'string'));
|
|
76
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256 } from './hash.js';
|
|
4
|
+
import { hasReviewedTemplateVersion } from './state.js';
|
|
5
|
+
import { renderTemplateForSync } from './templates.js';
|
|
6
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getWorkflowTargets, } from './workflow-targets.js';
|
|
7
|
+
const ACTIONABLE_SYNC_PREVIEW_STATUSES = new Set([
|
|
8
|
+
'new-template',
|
|
9
|
+
'missing',
|
|
10
|
+
'modified',
|
|
11
|
+
'update-available',
|
|
12
|
+
'modified-with-update',
|
|
13
|
+
'unknown-template',
|
|
14
|
+
]);
|
|
15
|
+
const PROMPTABLE_SYNC_PREVIEW_STATUSES = new Set([
|
|
16
|
+
'new-template',
|
|
17
|
+
'missing',
|
|
18
|
+
'modified',
|
|
19
|
+
'update-available',
|
|
20
|
+
'modified-with-update',
|
|
21
|
+
]);
|
|
22
|
+
export function isActionableSyncPreview(preview) {
|
|
23
|
+
return ACTIONABLE_SYNC_PREVIEW_STATUSES.has(preview.status);
|
|
24
|
+
}
|
|
25
|
+
export function shouldAskForSyncAction(preview) {
|
|
26
|
+
return PROMPTABLE_SYNC_PREVIEW_STATUSES.has(preview.status);
|
|
27
|
+
}
|
|
28
|
+
export function getSyncPreviews({ rootPath, state, manifest }) {
|
|
29
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
30
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
31
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
32
|
+
const previews = Object.entries(state.managedFiles).map(([relativePath, managedFile]) => getManagedFileSyncPreview({
|
|
33
|
+
rootPath,
|
|
34
|
+
manifest,
|
|
35
|
+
relativePath,
|
|
36
|
+
managedFile,
|
|
37
|
+
selectedLanguageSkills,
|
|
38
|
+
selectedFrameworkSkills,
|
|
39
|
+
selectedArchitectureSkill,
|
|
40
|
+
}));
|
|
41
|
+
const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill);
|
|
42
|
+
for (const target of expectedTargets) {
|
|
43
|
+
const relativePath = path.relative(rootPath, target.targetPath);
|
|
44
|
+
if (state.managedFiles[relativePath]) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const template = manifest.templates[target.templateRelativePath];
|
|
48
|
+
if (!template) {
|
|
49
|
+
previews.push({
|
|
50
|
+
relativePath,
|
|
51
|
+
managedFile: {
|
|
52
|
+
template: target.templateRelativePath,
|
|
53
|
+
templateVersion: 'unknown',
|
|
54
|
+
sha256: '',
|
|
55
|
+
},
|
|
56
|
+
status: 'unknown-template',
|
|
57
|
+
changes: [],
|
|
58
|
+
hasLocalFile: fs.existsSync(target.targetPath),
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
previews.push({
|
|
63
|
+
relativePath,
|
|
64
|
+
managedFile: {
|
|
65
|
+
template: target.templateRelativePath,
|
|
66
|
+
templateVersion: template.version,
|
|
67
|
+
sha256: '',
|
|
68
|
+
status: 'managed',
|
|
69
|
+
},
|
|
70
|
+
status: 'new-template',
|
|
71
|
+
currentTemplateVersion: template.version,
|
|
72
|
+
changes: template.changes,
|
|
73
|
+
hasLocalFile: fs.existsSync(target.targetPath),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return previews;
|
|
77
|
+
}
|
|
78
|
+
function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, }) {
|
|
79
|
+
if (managedFile.status === 'unmanaged') {
|
|
80
|
+
return { relativePath, managedFile, status: 'unmanaged', recordedTemplateVersion: managedFile.templateVersion, changes: [] };
|
|
81
|
+
}
|
|
82
|
+
const template = manifest.templates[managedFile.template];
|
|
83
|
+
if (!template) {
|
|
84
|
+
return { relativePath, managedFile, status: 'unknown-template', recordedTemplateVersion: managedFile.templateVersion, changes: [] };
|
|
85
|
+
}
|
|
86
|
+
const targetPath = path.join(rootPath, relativePath);
|
|
87
|
+
const hasLocalFile = fs.existsSync(targetPath);
|
|
88
|
+
const hasLocalEdits = hasLocalFile ? sha256(fs.readFileSync(targetPath, 'utf8')) !== managedFile.sha256 : false;
|
|
89
|
+
const hasTemplateUpdate = managedFile.templateVersion !== template.version && !hasReviewedTemplateVersion(managedFile, template.version);
|
|
90
|
+
const hasSelectionUpdate = hasRenderedSelectionUpdate({
|
|
91
|
+
relativePath,
|
|
92
|
+
currentPath: targetPath,
|
|
93
|
+
managedFile,
|
|
94
|
+
selectedLanguageSkills,
|
|
95
|
+
selectedFrameworkSkills,
|
|
96
|
+
selectedArchitectureSkill,
|
|
97
|
+
});
|
|
98
|
+
const hasUpdate = hasTemplateUpdate || hasSelectionUpdate;
|
|
99
|
+
const changes = hasTemplateUpdate ? template.changes : ['Refreshes generated skill routing for the current selected skills.'];
|
|
100
|
+
if (!hasLocalFile) {
|
|
101
|
+
return {
|
|
102
|
+
relativePath,
|
|
103
|
+
managedFile,
|
|
104
|
+
status: 'missing',
|
|
105
|
+
recordedTemplateVersion: managedFile.templateVersion,
|
|
106
|
+
currentTemplateVersion: template.version,
|
|
107
|
+
changes: template.changes,
|
|
108
|
+
hasLocalFile,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (hasLocalEdits && hasUpdate) {
|
|
112
|
+
return {
|
|
113
|
+
relativePath,
|
|
114
|
+
managedFile,
|
|
115
|
+
status: 'modified-with-update',
|
|
116
|
+
recordedTemplateVersion: managedFile.templateVersion,
|
|
117
|
+
currentTemplateVersion: template.version,
|
|
118
|
+
changes,
|
|
119
|
+
hasLocalFile,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (hasLocalEdits) {
|
|
123
|
+
return {
|
|
124
|
+
relativePath,
|
|
125
|
+
managedFile,
|
|
126
|
+
status: 'modified',
|
|
127
|
+
recordedTemplateVersion: managedFile.templateVersion,
|
|
128
|
+
currentTemplateVersion: template.version,
|
|
129
|
+
changes: [],
|
|
130
|
+
hasLocalFile,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (hasUpdate) {
|
|
134
|
+
return {
|
|
135
|
+
relativePath,
|
|
136
|
+
managedFile,
|
|
137
|
+
status: 'update-available',
|
|
138
|
+
recordedTemplateVersion: managedFile.templateVersion,
|
|
139
|
+
currentTemplateVersion: template.version,
|
|
140
|
+
changes,
|
|
141
|
+
hasLocalFile,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
relativePath,
|
|
146
|
+
managedFile,
|
|
147
|
+
status: 'up-to-date',
|
|
148
|
+
recordedTemplateVersion: managedFile.templateVersion,
|
|
149
|
+
currentTemplateVersion: template.version,
|
|
150
|
+
changes: [],
|
|
151
|
+
hasLocalFile,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function hasRenderedSelectionUpdate({ relativePath, currentPath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, }) {
|
|
155
|
+
if (relativePath !== 'AGENTS.md') {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const renderedContents = renderTemplateForSync({
|
|
159
|
+
templateRelativePath: managedFile.template,
|
|
160
|
+
currentPath,
|
|
161
|
+
selectedLanguageSkills,
|
|
162
|
+
selectedFrameworkSkills,
|
|
163
|
+
selectedArchitectureSkill,
|
|
164
|
+
});
|
|
165
|
+
return sha256(renderedContents) !== managedFile.sha256;
|
|
166
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getTemplatesPath } from './paths.js';
|
|
4
|
+
export function readTemplate(relativePath) {
|
|
5
|
+
return fs.readFileSync(path.join(getTemplatesPath(), relativePath), 'utf8');
|
|
6
|
+
}
|
|
7
|
+
export function readTemplateManifest() {
|
|
8
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(getTemplatesPath(), 'manifest.json'), 'utf8'));
|
|
9
|
+
if (!isTemplateManifest(manifest)) {
|
|
10
|
+
throw new Error('templates/manifest.json is not a valid template manifest.');
|
|
11
|
+
}
|
|
12
|
+
return manifest;
|
|
13
|
+
}
|
|
14
|
+
export function getTemplateVersion(manifest, templateRelativePath) {
|
|
15
|
+
const template = manifest.templates[templateRelativePath];
|
|
16
|
+
if (!template) {
|
|
17
|
+
throw new Error(`Template ${templateRelativePath} is missing from templates/manifest.json.`);
|
|
18
|
+
}
|
|
19
|
+
return template.version;
|
|
20
|
+
}
|
|
21
|
+
export function renderTemplateForSync({ templateRelativePath, currentPath, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, }) {
|
|
22
|
+
let contents = readTemplate(templateRelativePath);
|
|
23
|
+
if (contents.includes('{{PROJECT_OVERVIEW}}')) {
|
|
24
|
+
contents = contents.replace('{{PROJECT_OVERVIEW}}', extractProjectOverview(currentPath) ?? 'Describe this project.');
|
|
25
|
+
}
|
|
26
|
+
return renderSkillRouting(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill);
|
|
27
|
+
}
|
|
28
|
+
export function renderSkillRouting(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill) {
|
|
29
|
+
if (!contents.includes('{{OPTIONAL_SKILL_ROUTING}}')) {
|
|
30
|
+
return contents;
|
|
31
|
+
}
|
|
32
|
+
const optionalRouting = [...selectedLanguageSkills, ...selectedFrameworkSkills, ...(selectedArchitectureSkill ? [selectedArchitectureSkill] : [])]
|
|
33
|
+
.map((skill) => `- ${skill.routingInstruction}`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
return contents.replace('{{OPTIONAL_SKILL_ROUTING}}', optionalRouting);
|
|
36
|
+
}
|
|
37
|
+
export function extractProjectOverview(filePath) {
|
|
38
|
+
if (!fs.existsSync(filePath)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const contents = fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
const match = contents.match(/## Project overview\s+([\s\S]*?)(?=\n## |$)/);
|
|
43
|
+
const overview = match?.[1]?.trim();
|
|
44
|
+
return overview || undefined;
|
|
45
|
+
}
|
|
46
|
+
function isTemplateManifest(value) {
|
|
47
|
+
if (!value || typeof value !== 'object') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const manifest = value;
|
|
51
|
+
return (typeof manifest.templateVersion === 'string' &&
|
|
52
|
+
!!manifest.templates &&
|
|
53
|
+
typeof manifest.templates === 'object' &&
|
|
54
|
+
Object.values(manifest.templates).every((template) => !!template &&
|
|
55
|
+
typeof template === 'object' &&
|
|
56
|
+
typeof template.version === 'string' &&
|
|
57
|
+
typeof template.description === 'string' &&
|
|
58
|
+
Array.isArray(template.changes) &&
|
|
59
|
+
template.changes.every((change) => typeof change === 'string')));
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readStateForDoctor } from './state.js';
|
|
2
|
+
import { getSyncPreviews, isActionableSyncPreview } from './sync-preview.js';
|
|
3
|
+
import { readTemplateManifest } from './templates.js';
|
|
4
|
+
export function getWorkflowMutationReadiness({ rootPath, statePath }) {
|
|
5
|
+
const stateResult = readStateForDoctor(statePath);
|
|
6
|
+
if (!stateResult.ok) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
message: stateResult.message,
|
|
10
|
+
hint: 'Run `sibu init` before selecting project skills.',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const manifest = readTemplateManifest();
|
|
14
|
+
const previews = getSyncPreviews({ rootPath, state: stateResult.state, manifest });
|
|
15
|
+
const actionablePreviews = previews.filter(isActionableSyncPreview);
|
|
16
|
+
if (actionablePreviews.length > 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
message: 'Workflow state is not clean enough to select a skill safely.',
|
|
20
|
+
hint: 'Run `sibu sync` to review workflow state before selecting a skill.',
|
|
21
|
+
actionablePreviews,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
state: stateResult.state,
|
|
27
|
+
manifest,
|
|
28
|
+
previews,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SIBU_VERSION, MANDATORY_SKILLS, SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SUPPORTED_AGENTS } from './catalog.js';
|
|
4
|
+
import { sha256 } from './hash.js';
|
|
5
|
+
import { removeUndefinedFields } from './object.js';
|
|
6
|
+
import { readExistingState } from './state.js';
|
|
7
|
+
import { getTemplateVersion, readTemplate, readTemplateManifest, renderSkillRouting } from './templates.js';
|
|
8
|
+
export function getSelectedLanguageSkillsFromState(state) {
|
|
9
|
+
return SELECTABLE_LANGUAGE_SKILLS.filter((skill) => state.selectedLanguageSkills?.includes(skill.id));
|
|
10
|
+
}
|
|
11
|
+
export function getSelectedFrameworkSkillsFromState(state) {
|
|
12
|
+
return SELECTABLE_FRAMEWORK_SKILLS.filter((skill) => state.selectedFrameworkSkills?.includes(skill.id));
|
|
13
|
+
}
|
|
14
|
+
export function getSelectedArchitectureSkillFromState(state) {
|
|
15
|
+
return SELECTABLE_ARCHITECTURE_SKILLS.find((skill) => skill.id === state.selectedArchitectureSkill);
|
|
16
|
+
}
|
|
17
|
+
export function getSelectedSkillTargetsForAgents(selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill) {
|
|
18
|
+
const skillTargets = new Map();
|
|
19
|
+
const selectedSkills = [
|
|
20
|
+
...MANDATORY_SKILLS,
|
|
21
|
+
...selectedLanguageSkills,
|
|
22
|
+
...selectedFrameworkSkills,
|
|
23
|
+
...(selectedArchitectureSkill ? [selectedArchitectureSkill] : []),
|
|
24
|
+
];
|
|
25
|
+
for (const agent of selectedAgents) {
|
|
26
|
+
for (const skill of selectedSkills) {
|
|
27
|
+
const targetRelativePath = skill.targetRelativePathsByAgent[agent.id];
|
|
28
|
+
if (!targetRelativePath) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
skillTargets.set(targetRelativePath, {
|
|
32
|
+
targetRelativePath,
|
|
33
|
+
templateRelativePath: skill.templateRelativePath,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...skillTargets.values()];
|
|
38
|
+
}
|
|
39
|
+
export function getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills = [], selectedFrameworkSkills = [], selectedArchitectureSkill) {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
label: 'AGENTS.md',
|
|
43
|
+
targetPath: path.join(rootPath, 'AGENTS.md'),
|
|
44
|
+
templateRelativePath: 'AGENTS.md',
|
|
45
|
+
requiresProjectOverview: true,
|
|
46
|
+
},
|
|
47
|
+
...selectedAgents.flatMap((agent) => {
|
|
48
|
+
if (!agent.targetRelativePath || !agent.templateRelativePath) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
label: agent.targetRelativePath,
|
|
54
|
+
targetPath: path.join(rootPath, agent.targetRelativePath),
|
|
55
|
+
templateRelativePath: agent.templateRelativePath,
|
|
56
|
+
requiresProjectOverview: false,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}),
|
|
60
|
+
...getSelectedSkillTargetsForAgents(selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill).map((skillTarget) => ({
|
|
61
|
+
label: skillTarget.targetRelativePath,
|
|
62
|
+
targetPath: path.join(rootPath, skillTarget.targetRelativePath),
|
|
63
|
+
templateRelativePath: skillTarget.templateRelativePath,
|
|
64
|
+
requiresProjectOverview: false,
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
export function renderMissingWorkflowFiles({ missingTargets, overview, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, }) {
|
|
69
|
+
return missingTargets.map((target) => {
|
|
70
|
+
let contents = readTemplate(target.templateRelativePath);
|
|
71
|
+
if (target.requiresProjectOverview) {
|
|
72
|
+
if (!overview?.trim()) {
|
|
73
|
+
throw new Error('Project overview is required to create AGENTS.md.');
|
|
74
|
+
}
|
|
75
|
+
contents = contents.replace('{{PROJECT_OVERVIEW}}', overview.trim());
|
|
76
|
+
}
|
|
77
|
+
contents = renderSkillRouting(contents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill);
|
|
78
|
+
return {
|
|
79
|
+
label: target.label,
|
|
80
|
+
targetPath: target.targetPath,
|
|
81
|
+
contents,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function writeSibuState({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, targets, }) {
|
|
86
|
+
const previousState = readExistingState(statePath);
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const manifest = readTemplateManifest();
|
|
89
|
+
const state = {
|
|
90
|
+
sibuVersion: SIBU_VERSION,
|
|
91
|
+
templateVersion: manifest.templateVersion,
|
|
92
|
+
generatedAt: previousState?.generatedAt ?? now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
selectedAgents: selectedAgents.map((agent) => agent.id),
|
|
95
|
+
selectedLanguageSkills: selectedLanguageSkills.map((skill) => skill.id),
|
|
96
|
+
selectedFrameworkSkills: selectedFrameworkSkills.map((skill) => skill.id),
|
|
97
|
+
selectedArchitectureSkill: selectedArchitectureSkill?.id,
|
|
98
|
+
managedFiles: Object.fromEntries(targets
|
|
99
|
+
.filter((target) => fs.existsSync(target.targetPath))
|
|
100
|
+
.map((target) => {
|
|
101
|
+
const relativePath = path.relative(rootPath, target.targetPath);
|
|
102
|
+
const previousManagedFile = previousState?.managedFiles[relativePath];
|
|
103
|
+
const nextManagedFile = {
|
|
104
|
+
template: target.templateRelativePath,
|
|
105
|
+
templateVersion: getTemplateVersion(manifest, target.templateRelativePath),
|
|
106
|
+
sha256: sha256(fs.readFileSync(target.targetPath, 'utf8')),
|
|
107
|
+
status: previousManagedFile?.status ?? 'managed',
|
|
108
|
+
lastReviewedTemplateVersion: previousManagedFile?.lastReviewedTemplateVersion,
|
|
109
|
+
reason: previousManagedFile?.reason,
|
|
110
|
+
};
|
|
111
|
+
return [relativePath, removeUndefinedFields(nextManagedFile)];
|
|
112
|
+
})),
|
|
113
|
+
};
|
|
114
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
115
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
export function getSelectedAgentsFromState(state) {
|
|
118
|
+
return SUPPORTED_AGENTS.filter((agent) => state.selectedAgents.includes(agent.id));
|
|
119
|
+
}
|