@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,213 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cancel, intro, isCancel, log, outro, select } from '@clack/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { STATE_RELATIVE_PATH, resolveSelectableSkillById } from '../../shared/catalog.js';
|
|
6
|
+
import { readFileHashIfPresent, sha256 } from '../../shared/hash.js';
|
|
7
|
+
import { getProjectContext } from '../../shared/paths.js';
|
|
8
|
+
import { renderIntro } from '../../shared/prompts.js';
|
|
9
|
+
import { cloneState, readStateForDoctor, writeStateFile } from '../../shared/state.js';
|
|
10
|
+
import { getTemplateVersion, readTemplateManifest, renderTemplateForSync } from '../../shared/templates.js';
|
|
11
|
+
import { removeUndefinedFields } from '../../shared/object.js';
|
|
12
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, } from '../../shared/workflow-targets.js';
|
|
13
|
+
export async function handleStopManagingFile({ skillName }) {
|
|
14
|
+
await renderIntro();
|
|
15
|
+
intro(chalk.cyan('Updating skill management'));
|
|
16
|
+
const { rootPath, statePath } = getProjectContext();
|
|
17
|
+
const stateResult = readStateForDoctor(statePath);
|
|
18
|
+
if (!stateResult.ok) {
|
|
19
|
+
log.error(stateResult.message);
|
|
20
|
+
log.info('Run `sibu init` before managing workflow skill state.');
|
|
21
|
+
outro(chalk.yellow('Skill update unavailable.'));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const result = stopSelectedSkill({ rootPath, state: stateResult.state, skillName });
|
|
26
|
+
switch (result.status) {
|
|
27
|
+
case 'blocked':
|
|
28
|
+
log.error(result.message);
|
|
29
|
+
if (result.hint) {
|
|
30
|
+
log.info(result.hint);
|
|
31
|
+
}
|
|
32
|
+
outro(chalk.yellow('Nothing changed.'));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
case 'noop':
|
|
36
|
+
log.success(result.message);
|
|
37
|
+
log.info('No files changed.');
|
|
38
|
+
outro(chalk.green('Skill selection is already up to date.'));
|
|
39
|
+
return;
|
|
40
|
+
case 'stopped':
|
|
41
|
+
writeStateFile(statePath, result.state);
|
|
42
|
+
for (const stoppedPath of result.stoppedPaths) {
|
|
43
|
+
log.success(`Stopped managing ${stoppedPath.relativePath}.`);
|
|
44
|
+
}
|
|
45
|
+
log.success('Updated AGENTS.md skill routing.');
|
|
46
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}.`);
|
|
47
|
+
for (const [index, stoppedPath] of result.stoppedPaths.entries()) {
|
|
48
|
+
await askToDeleteStoppedFile(stoppedPath, result.stoppedFiles[index]);
|
|
49
|
+
}
|
|
50
|
+
outro(chalk.green(`Updated ${result.skillName}.`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function stopSelectedSkill({ rootPath, state, skillName }) {
|
|
55
|
+
const resolution = resolveSelectableSkillById(skillName);
|
|
56
|
+
if (!resolution.ok) {
|
|
57
|
+
return { status: 'blocked', message: resolution.message, hint: 'Use `sibu skills stop <skill_name>` with a selectable skill id from `sibu skills list`.' };
|
|
58
|
+
}
|
|
59
|
+
if (!isSkillSelected(state, resolution.resolved)) {
|
|
60
|
+
return { status: 'noop', message: `${resolution.resolved.skill.name} is not selected.` };
|
|
61
|
+
}
|
|
62
|
+
const stoppedPaths = getSkillManagedPaths(rootPath, state, resolution.resolved);
|
|
63
|
+
if (stoppedPaths.length === 0) {
|
|
64
|
+
return { status: 'blocked', message: `${resolution.resolved.skill.name} does not have a managed target for the selected agents.` };
|
|
65
|
+
}
|
|
66
|
+
const nextState = cloneState(state);
|
|
67
|
+
const stoppedFiles = [];
|
|
68
|
+
for (const stoppedPath of stoppedPaths) {
|
|
69
|
+
const managedFile = nextState.managedFiles[stoppedPath.relativePath];
|
|
70
|
+
if (!managedFile) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'blocked',
|
|
73
|
+
message: `${stoppedPath.relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
|
|
74
|
+
hint: 'Run `sibu sync` to review workflow state before stopping this skill.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const currentHash = readFileHashIfPresent(stoppedPath.absolutePath);
|
|
78
|
+
const stoppedFile = removeUndefinedFields({
|
|
79
|
+
...managedFile,
|
|
80
|
+
sha256: currentHash ?? managedFile.sha256,
|
|
81
|
+
status: 'unmanaged',
|
|
82
|
+
reason: 'Stopped by `sibu skills stop`.',
|
|
83
|
+
});
|
|
84
|
+
nextState.managedFiles[stoppedPath.relativePath] = stoppedFile;
|
|
85
|
+
stoppedFiles.push(stoppedFile);
|
|
86
|
+
}
|
|
87
|
+
removeSelectedSkill(nextState, resolution.resolved);
|
|
88
|
+
const agentsUpdate = getAgentsUpdate(rootPath, nextState);
|
|
89
|
+
if (!agentsUpdate.ok) {
|
|
90
|
+
return { status: 'blocked', message: agentsUpdate.message, hint: 'Run `sibu sync` to review workflow state before stopping this skill.' };
|
|
91
|
+
}
|
|
92
|
+
fs.writeFileSync(agentsUpdate.path, agentsUpdate.contents, 'utf8');
|
|
93
|
+
const manifest = readTemplateManifest();
|
|
94
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
95
|
+
nextState.updatedAt = new Date().toISOString();
|
|
96
|
+
nextState.managedFiles['AGENTS.md'] = removeUndefinedFields({
|
|
97
|
+
...agentsUpdate.managedFile,
|
|
98
|
+
templateVersion: getTemplateVersion(manifest, agentsUpdate.managedFile.template),
|
|
99
|
+
sha256: sha256(agentsUpdate.contents),
|
|
100
|
+
status: agentsUpdate.managedFile.status ?? 'managed',
|
|
101
|
+
});
|
|
102
|
+
return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, skillName: resolution.resolved.skill.name };
|
|
103
|
+
}
|
|
104
|
+
function isSkillSelected(state, resolved) {
|
|
105
|
+
switch (resolved.kind) {
|
|
106
|
+
case 'language':
|
|
107
|
+
return state.selectedLanguageSkills?.includes(resolved.skill.id) ?? false;
|
|
108
|
+
case 'framework':
|
|
109
|
+
return state.selectedFrameworkSkills?.includes(resolved.skill.id) ?? false;
|
|
110
|
+
case 'architecture':
|
|
111
|
+
return state.selectedArchitectureSkill === resolved.skill.id;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function removeSelectedSkill(state, resolved) {
|
|
115
|
+
switch (resolved.kind) {
|
|
116
|
+
case 'language':
|
|
117
|
+
state.selectedLanguageSkills = (state.selectedLanguageSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
118
|
+
return;
|
|
119
|
+
case 'framework':
|
|
120
|
+
state.selectedFrameworkSkills = (state.selectedFrameworkSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
121
|
+
return;
|
|
122
|
+
case 'architecture':
|
|
123
|
+
delete state.selectedArchitectureSkill;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function getSkillManagedPaths(rootPath, state, resolved) {
|
|
128
|
+
const relativePaths = new Set();
|
|
129
|
+
for (const agent of getSelectedAgentsFromState(state)) {
|
|
130
|
+
const relativePath = resolved.skill.targetRelativePathsByAgent[agent.id];
|
|
131
|
+
if (relativePath) {
|
|
132
|
+
relativePaths.add(relativePath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...relativePaths].map((relativePath) => ({
|
|
136
|
+
relativePath,
|
|
137
|
+
absolutePath: path.join(rootPath, relativePath),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function getAgentsUpdate(rootPath, state) {
|
|
141
|
+
const agentsRelativePath = 'AGENTS.md';
|
|
142
|
+
const agentsPath = path.join(rootPath, agentsRelativePath);
|
|
143
|
+
const managedFile = state.managedFiles[agentsRelativePath];
|
|
144
|
+
if (!managedFile) {
|
|
145
|
+
return { ok: false, message: 'AGENTS.md is not recorded in `.sibu/state.json`.' };
|
|
146
|
+
}
|
|
147
|
+
if (!fs.existsSync(agentsPath)) {
|
|
148
|
+
return { ok: false, message: 'AGENTS.md is missing.' };
|
|
149
|
+
}
|
|
150
|
+
const currentHash = sha256(fs.readFileSync(agentsPath, 'utf8'));
|
|
151
|
+
if (currentHash !== managedFile.sha256) {
|
|
152
|
+
return { ok: false, message: 'AGENTS.md has changed since Sibu last recorded it.' };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
path: agentsPath,
|
|
157
|
+
managedFile,
|
|
158
|
+
contents: renderTemplateForSync({
|
|
159
|
+
templateRelativePath: managedFile.template,
|
|
160
|
+
currentPath: agentsPath,
|
|
161
|
+
selectedLanguageSkills: getSelectedLanguageSkillsFromState(state),
|
|
162
|
+
selectedFrameworkSkills: getSelectedFrameworkSkillsFromState(state),
|
|
163
|
+
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(state),
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function askToDeleteStoppedFile(managedPath, managedFile) {
|
|
168
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
169
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
173
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const deleteAction = await select({
|
|
177
|
+
message: `Do you also want to delete ${managedPath.relativePath} for good?`,
|
|
178
|
+
options: [
|
|
179
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local file unchanged.' },
|
|
180
|
+
{ value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
if (isCancel(deleteAction)) {
|
|
184
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
if (deleteAction === 'keep') {
|
|
188
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (hasLocalEdits(managedPath.absolutePath, managedFile)) {
|
|
192
|
+
const confirmDelete = await select({
|
|
193
|
+
message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
|
|
194
|
+
options: [
|
|
195
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
|
|
196
|
+
{ value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
if (isCancel(confirmDelete)) {
|
|
200
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
if (confirmDelete === 'keep') {
|
|
204
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
fs.unlinkSync(managedPath.absolutePath);
|
|
209
|
+
log.success(`Deleted ${managedPath.relativePath}.`);
|
|
210
|
+
}
|
|
211
|
+
function hasLocalEdits(filePath, managedFile) {
|
|
212
|
+
return readFileHashIfPresent(filePath) !== managedFile.sha256;
|
|
213
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cancel, isCancel, select } from '@clack/prompts';
|
|
2
|
+
export async function askForSyncAction(preview) {
|
|
3
|
+
if (preview.status === 'update-available') {
|
|
4
|
+
const action = await select({
|
|
5
|
+
message: `What should I do with ${preview.relativePath}?`,
|
|
6
|
+
options: [
|
|
7
|
+
{ value: 'apply-update', label: 'Apply update', hint: 'Safe because no local edits were detected.' },
|
|
8
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
9
|
+
],
|
|
10
|
+
});
|
|
11
|
+
return handleSyncActionCancel(action);
|
|
12
|
+
}
|
|
13
|
+
if (preview.status === 'missing') {
|
|
14
|
+
const action = await select({
|
|
15
|
+
message: `What should I do with ${preview.relativePath}?`,
|
|
16
|
+
options: [
|
|
17
|
+
{ value: 'apply-update', label: 'Recreate file', hint: 'Write the latest template and update Sibu state.' },
|
|
18
|
+
{ value: 'stop-managing', label: 'Stop managing this file', hint: 'Opt this file out of Sibu missing-file warnings.' },
|
|
19
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
return handleSyncActionCancel(action);
|
|
23
|
+
}
|
|
24
|
+
if (preview.status === 'new-template') {
|
|
25
|
+
const options = preview.hasLocalFile
|
|
26
|
+
? [
|
|
27
|
+
{ value: 'mark-reviewed', label: 'Start managing existing file', hint: 'Keep the file unchanged and record it in Sibu state.' },
|
|
28
|
+
{ value: 'write-side-template', label: 'Write latest template beside my file', hint: 'Create a reference copy under .sibu/sync/.' },
|
|
29
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
30
|
+
]
|
|
31
|
+
: [
|
|
32
|
+
{ value: 'apply-update', label: 'Create file', hint: 'Write the latest template and record it in Sibu state.' },
|
|
33
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
34
|
+
];
|
|
35
|
+
const action = await select({
|
|
36
|
+
message: `What should I do with ${preview.relativePath}?`,
|
|
37
|
+
options,
|
|
38
|
+
});
|
|
39
|
+
return handleSyncActionCancel(action);
|
|
40
|
+
}
|
|
41
|
+
const options = [
|
|
42
|
+
{ value: 'mark-reviewed', label: 'Mark as reviewed', hint: 'Keep my file and stop warning about this template version.' },
|
|
43
|
+
{ value: 'stop-managing', label: 'Stop managing this file', hint: 'Opt this file out of Sibu template drift warnings.' },
|
|
44
|
+
{ value: 'skip', label: 'Skip for now' },
|
|
45
|
+
];
|
|
46
|
+
if (preview.status === 'modified-with-update') {
|
|
47
|
+
options.splice(1, 0, {
|
|
48
|
+
value: 'write-side-template',
|
|
49
|
+
label: 'Write latest template beside my file',
|
|
50
|
+
hint: 'Create a reference copy under .sibu/sync/.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const action = await select({
|
|
54
|
+
message: `What should I do with ${preview.relativePath}?`,
|
|
55
|
+
options,
|
|
56
|
+
});
|
|
57
|
+
return handleSyncActionCancel(action);
|
|
58
|
+
}
|
|
59
|
+
function handleSyncActionCancel(action) {
|
|
60
|
+
if (isCancel(action)) {
|
|
61
|
+
cancel('Sync cancelled.');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
return action;
|
|
65
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log } from '@clack/prompts';
|
|
4
|
+
import { sha256 } from '../../shared/hash.js';
|
|
5
|
+
import { getSideTemplatePath } from '../../shared/paths.js';
|
|
6
|
+
import { cloneState } from '../../shared/state.js';
|
|
7
|
+
import { getTemplateVersion, renderTemplateForSync } from '../../shared/templates.js';
|
|
8
|
+
import { getSelectedArchitectureSkillFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState } from '../../shared/workflow-targets.js';
|
|
9
|
+
export function applySyncAction({ rootPath, state, manifest, preview, action, }) {
|
|
10
|
+
const nextState = cloneState(state);
|
|
11
|
+
const managedFile = nextState.managedFiles[preview.relativePath] ?? preview.managedFile;
|
|
12
|
+
const targetPath = path.join(rootPath, preview.relativePath);
|
|
13
|
+
const currentTemplateVersion = preview.currentTemplateVersion ?? getTemplateVersion(manifest, managedFile.template);
|
|
14
|
+
switch (action) {
|
|
15
|
+
case 'apply-update': {
|
|
16
|
+
const contents = renderTemplateForSync({
|
|
17
|
+
templateRelativePath: managedFile.template,
|
|
18
|
+
currentPath: targetPath,
|
|
19
|
+
selectedLanguageSkills: getSelectedLanguageSkillsFromState(nextState),
|
|
20
|
+
selectedFrameworkSkills: getSelectedFrameworkSkillsFromState(nextState),
|
|
21
|
+
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(nextState),
|
|
22
|
+
});
|
|
23
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
24
|
+
fs.writeFileSync(targetPath, contents, 'utf8');
|
|
25
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
26
|
+
nextState.updatedAt = new Date().toISOString();
|
|
27
|
+
nextState.managedFiles[preview.relativePath] = {
|
|
28
|
+
template: managedFile.template,
|
|
29
|
+
templateVersion: currentTemplateVersion,
|
|
30
|
+
sha256: sha256(contents),
|
|
31
|
+
status: 'managed',
|
|
32
|
+
};
|
|
33
|
+
log.success(`Applied latest template to ${preview.relativePath}`);
|
|
34
|
+
return { state: nextState, changedFiles: true, changedState: true };
|
|
35
|
+
}
|
|
36
|
+
case 'mark-reviewed': {
|
|
37
|
+
const contents = fs.readFileSync(targetPath, 'utf8');
|
|
38
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
39
|
+
nextState.updatedAt = new Date().toISOString();
|
|
40
|
+
nextState.managedFiles[preview.relativePath] = {
|
|
41
|
+
...managedFile,
|
|
42
|
+
sha256: sha256(contents),
|
|
43
|
+
status: 'customized',
|
|
44
|
+
lastReviewedTemplateVersion: currentTemplateVersion,
|
|
45
|
+
};
|
|
46
|
+
log.success(`Marked ${preview.relativePath} as reviewed.`);
|
|
47
|
+
return { state: nextState, changedFiles: false, changedState: true };
|
|
48
|
+
}
|
|
49
|
+
case 'write-side-template': {
|
|
50
|
+
const sideTemplatePath = getSideTemplatePath(rootPath, preview.relativePath, currentTemplateVersion);
|
|
51
|
+
const contents = renderTemplateForSync({
|
|
52
|
+
templateRelativePath: managedFile.template,
|
|
53
|
+
currentPath: targetPath,
|
|
54
|
+
selectedLanguageSkills: getSelectedLanguageSkillsFromState(state),
|
|
55
|
+
selectedFrameworkSkills: getSelectedFrameworkSkillsFromState(state),
|
|
56
|
+
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(state),
|
|
57
|
+
});
|
|
58
|
+
fs.mkdirSync(path.dirname(sideTemplatePath), { recursive: true });
|
|
59
|
+
fs.writeFileSync(sideTemplatePath, contents, 'utf8');
|
|
60
|
+
log.success(`Wrote latest template to ${path.relative(rootPath, sideTemplatePath)}`);
|
|
61
|
+
log.info('Review it and copy over anything you want.');
|
|
62
|
+
return { state, changedFiles: true, changedState: false };
|
|
63
|
+
}
|
|
64
|
+
case 'stop-managing': {
|
|
65
|
+
const contents = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, 'utf8') : '';
|
|
66
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
67
|
+
nextState.updatedAt = new Date().toISOString();
|
|
68
|
+
nextState.managedFiles[preview.relativePath] = {
|
|
69
|
+
...managedFile,
|
|
70
|
+
sha256: sha256(contents),
|
|
71
|
+
status: 'unmanaged',
|
|
72
|
+
lastReviewedTemplateVersion: currentTemplateVersion,
|
|
73
|
+
reason: 'user opted out',
|
|
74
|
+
};
|
|
75
|
+
log.success(`Stopped managing ${preview.relativePath}.`);
|
|
76
|
+
return { state: nextState, changedFiles: false, changedState: true };
|
|
77
|
+
}
|
|
78
|
+
case 'skip':
|
|
79
|
+
return { state, changedFiles: false, changedState: false };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { intro, log, outro } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
4
|
+
import { getProjectContext } from '../../shared/paths.js';
|
|
5
|
+
import { askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, renderIntro } from '../../shared/prompts.js';
|
|
6
|
+
import { readStateForDoctor, writeStateFile } from '../../shared/state.js';
|
|
7
|
+
import { readTemplateManifest } from '../../shared/templates.js';
|
|
8
|
+
import { askForSyncAction } from './action-prompt.js';
|
|
9
|
+
import { applySyncAction } from './apply-action.js';
|
|
10
|
+
import { logSyncPreview } from './log-preview.js';
|
|
11
|
+
import { getSyncPreviews, isActionableSyncPreview, shouldAskForSyncAction } from './preview.js';
|
|
12
|
+
export async function handleSyncProject(_command) {
|
|
13
|
+
await renderIntro();
|
|
14
|
+
intro(chalk.cyan('Reviewing workflow updates'));
|
|
15
|
+
const { rootPath, statePath } = getProjectContext();
|
|
16
|
+
const stateResult = readStateForDoctor(statePath);
|
|
17
|
+
if (!stateResult.ok) {
|
|
18
|
+
log.error(stateResult.message);
|
|
19
|
+
log.info('Run `sibu init` before syncing so I know which files are managed.');
|
|
20
|
+
outro(chalk.yellow('Sync unavailable.'));
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const languageSkillSelection = await askForNewLanguageSkills(stateResult.state);
|
|
25
|
+
const frameworkSkillSelection = await askForMissingFrameworkSkills(languageSkillSelection.state);
|
|
26
|
+
const architectureSkillSelection = await askForNewArchitectureSkill(frameworkSkillSelection.state);
|
|
27
|
+
let state = architectureSkillSelection.state;
|
|
28
|
+
const manifest = readTemplateManifest();
|
|
29
|
+
const previews = getSyncPreviews({ rootPath, state, manifest });
|
|
30
|
+
const actionablePreviews = previews.filter(isActionableSyncPreview);
|
|
31
|
+
if (actionablePreviews.length === 0) {
|
|
32
|
+
log.success('No template updates or local changes need review.');
|
|
33
|
+
if (state.templateVersion !== manifest.templateVersion ||
|
|
34
|
+
languageSkillSelection.changedState ||
|
|
35
|
+
frameworkSkillSelection.changedState ||
|
|
36
|
+
architectureSkillSelection.changedState) {
|
|
37
|
+
state = {
|
|
38
|
+
...state,
|
|
39
|
+
templateVersion: manifest.templateVersion,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
writeStateFile(statePath, state);
|
|
43
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
log.info('No files changed.');
|
|
47
|
+
}
|
|
48
|
+
outro(chalk.green('Everything is already in sync.'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
log.warn('Found workflow updates to review.');
|
|
52
|
+
let changedState = languageSkillSelection.changedState || frameworkSkillSelection.changedState || architectureSkillSelection.changedState;
|
|
53
|
+
let changedFiles = false;
|
|
54
|
+
for (const preview of previews) {
|
|
55
|
+
logSyncPreview(preview);
|
|
56
|
+
if (!shouldAskForSyncAction(preview)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const action = await askForSyncAction(preview);
|
|
60
|
+
if (action === 'skip') {
|
|
61
|
+
log.info(`Skipped ${preview.relativePath}.`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const result = applySyncAction({ rootPath, state, manifest, preview, action });
|
|
65
|
+
state = result.state;
|
|
66
|
+
changedState = changedState || result.changedState;
|
|
67
|
+
changedFiles = changedFiles || result.changedFiles;
|
|
68
|
+
}
|
|
69
|
+
if (changedState) {
|
|
70
|
+
writeStateFile(statePath, state);
|
|
71
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
72
|
+
}
|
|
73
|
+
if (!changedFiles && !changedState) {
|
|
74
|
+
log.info('No files changed.');
|
|
75
|
+
}
|
|
76
|
+
outro(chalk.green('Sync complete.'));
|
|
77
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { log } from '@clack/prompts';
|
|
2
|
+
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
3
|
+
export function logSyncPreview(preview) {
|
|
4
|
+
switch (preview.status) {
|
|
5
|
+
case 'up-to-date':
|
|
6
|
+
log.success(`${preview.relativePath} is up to date.`);
|
|
7
|
+
return;
|
|
8
|
+
case 'new-template':
|
|
9
|
+
if (preview.hasLocalFile) {
|
|
10
|
+
log.warn(`${preview.relativePath} is expected but is not recorded in ${STATE_RELATIVE_PATH}.`);
|
|
11
|
+
log.info('I will not overwrite it automatically. You can start managing it or write the latest template beside it.');
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
log.warn(`${preview.relativePath} should now be part of this workflow.`);
|
|
15
|
+
}
|
|
16
|
+
logTemplateChanges(preview);
|
|
17
|
+
return;
|
|
18
|
+
case 'missing':
|
|
19
|
+
log.error(`${preview.relativePath} is missing.`);
|
|
20
|
+
log.info('You can recreate it from the latest template during this sync.');
|
|
21
|
+
return;
|
|
22
|
+
case 'modified':
|
|
23
|
+
log.warn(`${preview.relativePath} has local edits.`);
|
|
24
|
+
log.info('I will not overwrite it automatically. There are no newer template changes for it right now.');
|
|
25
|
+
return;
|
|
26
|
+
case 'update-available':
|
|
27
|
+
if (preview.recordedTemplateVersion === preview.currentTemplateVersion) {
|
|
28
|
+
log.warn(`${preview.relativePath} needs generated content updates from your current project selections.`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
log.warn(`${preview.relativePath} has a newer template available (${preview.recordedTemplateVersion} → ${preview.currentTemplateVersion}).`);
|
|
32
|
+
}
|
|
33
|
+
logTemplateChanges(preview);
|
|
34
|
+
return;
|
|
35
|
+
case 'modified-with-update':
|
|
36
|
+
if (preview.recordedTemplateVersion === preview.currentTemplateVersion) {
|
|
37
|
+
log.warn(`${preview.relativePath} has local edits and also needs generated content updates from your current project selections.`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
log.warn(`${preview.relativePath} has local edits and a newer template is available (${preview.recordedTemplateVersion} → ${preview.currentTemplateVersion}).`);
|
|
41
|
+
}
|
|
42
|
+
log.info('I will not overwrite it automatically. Review the template changes below and decide what to adopt.');
|
|
43
|
+
logTemplateChanges(preview);
|
|
44
|
+
return;
|
|
45
|
+
case 'unknown-template':
|
|
46
|
+
log.warn(`${preview.relativePath} references a template that is not in templates/manifest.json.`);
|
|
47
|
+
return;
|
|
48
|
+
case 'unmanaged':
|
|
49
|
+
log.info(`${preview.relativePath} is unmanaged. I will leave it alone.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function logTemplateChanges(preview) {
|
|
54
|
+
if (preview.changes.length === 0) {
|
|
55
|
+
log.info('No human-readable template changes were recorded for this template.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
log.info('New template changes:');
|
|
59
|
+
for (const change of preview.changes) {
|
|
60
|
+
log.info(`- ${change}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getSyncPreviews, isActionableSyncPreview, shouldAskForSyncAction } from '../../shared/sync-preview.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|