@juancr11/sibu 0.6.0 → 0.8.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/bin/admin/changelog.js +155 -0
- package/bin/admin/release.js +217 -0
- package/bin/entrypoints/cli/create-program.js +1 -1
- package/bin/entrypoints/cli/execute-command.js +6 -6
- package/bin/modules/interactive-guidance/index.js +1 -0
- package/bin/{shared → modules/interactive-guidance}/prompts.js +17 -1
- package/bin/modules/maintainer-release-support/generate-changelog/changelog-format.js +213 -0
- package/bin/modules/maintainer-release-support/generate-changelog/changelog-writer.js +91 -0
- package/bin/modules/maintainer-release-support/generate-changelog/git-history.js +93 -0
- package/bin/modules/maintainer-release-support/generate-changelog/handler.js +167 -0
- package/bin/modules/maintainer-release-support/generate-changelog/semver.js +40 -0
- package/bin/modules/maintainer-release-support/index.js +2 -0
- package/bin/modules/maintainer-release-support/release-workflow/git-release.js +114 -0
- package/bin/modules/maintainer-release-support/release-workflow/handler.js +369 -0
- package/bin/modules/maintainer-release-support/release-workflow/package-json.js +181 -0
- package/bin/modules/maintainer-release-support/release-workflow/release-plan.js +87 -0
- package/bin/{features/init-project → modules/project-adoption}/handler.js +7 -5
- package/bin/modules/project-adoption/index.js +1 -0
- package/bin/modules/skill-selection-management/index.js +3 -0
- package/bin/{features → modules/skill-selection-management}/list-skills/handler.js +14 -4
- package/bin/{features → modules/skill-selection-management}/stop-managing-file/handler.js +15 -8
- package/bin/modules/skill-selection-management/use-skill/command.js +1 -0
- package/bin/{features → modules/skill-selection-management}/use-skill/handler.js +42 -8
- package/bin/{features/sync-project → modules/sync-review}/apply-action.js +5 -3
- package/bin/modules/sync-review/command.js +1 -0
- package/bin/{features/sync-project → modules/sync-review}/handler.js +4 -4
- package/bin/modules/sync-review/index.js +5 -0
- package/bin/{shared → modules/sync-review}/sync-preview.js +11 -7
- package/bin/modules/template-catalog-rendering/index.js +1 -0
- package/bin/modules/template-catalog-rendering/templates.js +60 -0
- package/bin/modules/version-advisory/index.js +1 -0
- package/bin/{shared → modules/version-advisory}/npm-version.js +29 -2
- package/bin/modules/workflow-health-diagnosis/command.js +1 -0
- package/bin/{features/doctor-project → modules/workflow-health-diagnosis}/handler.js +13 -7
- package/bin/modules/workflow-health-diagnosis/index.js +1 -0
- package/bin/modules/workflow-mutation-readiness/index.js +1 -0
- package/bin/{shared → modules/workflow-mutation-readiness}/workflow-mutation-readiness.js +3 -3
- package/bin/modules/workflow-state-registry/index.js +1 -0
- package/bin/{shared → modules/workflow-state-registry}/state.js +3 -1
- package/bin/modules/workflow-target-planning/catalog.js +252 -0
- package/bin/modules/workflow-target-planning/index.js +2 -0
- package/bin/modules/workflow-target-planning/workflow-targets.js +130 -0
- package/bin/shared/catalog.js +0 -253
- package/bin/shared/paths.js +1 -12
- package/bin/shared/workflow-targets.js +3 -2
- package/package.json +4 -2
- package/templates/AGENTS.md +3 -3
- package/templates/manifest.json +38 -27
- package/templates/skills/ai-implementation-plan-executor/SKILL.md +74 -70
- package/templates/skills/ai-implementation-planner/SKILL.md +24 -17
- package/templates/skills/architecture/command-pattern/SKILL.md +32 -20
- package/templates/skills/architecture/ddd-hexagonal/SKILL.md +14 -9
- package/templates/skills/deep-module-map-writer/SKILL.md +241 -0
- package/templates/skills/feature-brief-writer/SKILL.md +24 -24
- package/templates/skills/postgresql-expert/SKILL.md +72 -0
- package/templates/skills/product-vision-writer/SKILL.md +2 -2
- package/templates/skills/scrum-master-planner/SKILL.md +2 -2
- package/templates/skills/technical-design-writer/SKILL.md +15 -15
- package/templates/skills/ux-expert/SKILL.md +1 -1
- package/bin/features/sync-project/preview.js +0 -1
- package/templates/skills/product-context-map-writer/SKILL.md +0 -211
- /package/bin/{features/doctor-project/command.js → modules/cli-command-surface/index.js} +0 -0
- /package/bin/{features/init-project → modules/maintainer-release-support/generate-changelog}/command.js +0 -0
- /package/bin/{features/list-skills → modules/maintainer-release-support/release-workflow}/command.js +0 -0
- /package/bin/{features/stop-managing-file → modules/project-adoption}/command.js +0 -0
- /package/bin/{features/sync-project → modules/skill-selection-management/list-skills}/command.js +0 -0
- /package/bin/{features/use-skill → modules/skill-selection-management/stop-managing-file}/command.js +0 -0
- /package/bin/{features/sync-project → modules/sync-review}/action-prompt.js +0 -0
- /package/bin/{features/sync-project → modules/sync-review}/log-preview.js +0 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildChangelogProposal, retargetChangelogProposal } from '../generate-changelog/changelog-format.js';
|
|
4
|
+
import { planChangelogUpdate } from '../generate-changelog/changelog-writer.js';
|
|
5
|
+
import { readGitHistory } from '../generate-changelog/git-history.js';
|
|
6
|
+
import { parseSemverVersion } from '../generate-changelog/semver.js';
|
|
7
|
+
import { checkReleaseTagAvailable, resolveReleaseRange } from './git-release.js';
|
|
8
|
+
import { planPackageJsonVersionUpdate } from './package-json.js';
|
|
9
|
+
import { buildReleaseMetadataPlan, deriveSuggestedBumpFromChangelogProposal, formatReleaseTagName, incrementSemverVersion, renderReleasePlanPreview, extractReleaseChangelogSection, } from './release-plan.js';
|
|
10
|
+
export function planMaintainerRelease(command, cwd = process.cwd()) {
|
|
11
|
+
const releaseRange = resolveReleaseRange(command, cwd);
|
|
12
|
+
if (releaseRange.status === 'blocked') {
|
|
13
|
+
return releaseRange;
|
|
14
|
+
}
|
|
15
|
+
const gitHistory = readGitHistory({
|
|
16
|
+
fromRef: releaseRange.range.fromRef,
|
|
17
|
+
toRef: releaseRange.range.toRef,
|
|
18
|
+
}, cwd);
|
|
19
|
+
if (gitHistory.status === 'blocked') {
|
|
20
|
+
return {
|
|
21
|
+
status: 'blocked',
|
|
22
|
+
message: gitHistory.message,
|
|
23
|
+
warnings: gitHistory.warnings.map((warning) => ({ code: warning.code, message: warning.message })),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const commitDerivedChangelogProposal = buildChangelogProposal({
|
|
27
|
+
commits: gitHistory.commits,
|
|
28
|
+
sourceRange: gitHistory.sourceRange,
|
|
29
|
+
warnings: gitHistory.warnings,
|
|
30
|
+
});
|
|
31
|
+
const suggestedBump = deriveSuggestedBumpFromChangelogProposal(commitDerivedChangelogProposal);
|
|
32
|
+
const targetVersion = resolveTargetVersion({ fromRef: releaseRange.range.fromRef, requestedVersion: command.version, suggestedBump });
|
|
33
|
+
if (targetVersion.status === 'blocked') {
|
|
34
|
+
return targetVersion;
|
|
35
|
+
}
|
|
36
|
+
const targetDate = command.date ?? new Date().toISOString().slice(0, 10);
|
|
37
|
+
const changelogProposal = retargetChangelogProposal(commitDerivedChangelogProposal, {
|
|
38
|
+
type: 'version',
|
|
39
|
+
version: targetVersion.version,
|
|
40
|
+
date: targetDate,
|
|
41
|
+
});
|
|
42
|
+
const tagName = formatReleaseTagName(targetVersion.version);
|
|
43
|
+
const tagAvailability = checkReleaseTagAvailable(tagName, cwd);
|
|
44
|
+
if (tagAvailability.status === 'blocked') {
|
|
45
|
+
return tagAvailability;
|
|
46
|
+
}
|
|
47
|
+
const changelogUpdatePlan = planChangelogUpdate(readFileIfExists(path.join(cwd, 'CHANGELOG.md')), changelogProposal);
|
|
48
|
+
if (changelogUpdatePlan.status === 'blocked') {
|
|
49
|
+
return {
|
|
50
|
+
status: 'blocked',
|
|
51
|
+
message: changelogUpdatePlan.message,
|
|
52
|
+
warnings: changelogUpdatePlan.warnings.map((warning) => ({ code: warning.code, message: warning.message })),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const packageJsonUpdatePlan = planPackageJsonVersionUpdate(readFileIfExists(path.join(cwd, 'package.json')), targetVersion.version);
|
|
56
|
+
if (packageJsonUpdatePlan.status === 'blocked') {
|
|
57
|
+
return packageJsonUpdatePlan;
|
|
58
|
+
}
|
|
59
|
+
const existingChangelogSection = extractReleaseChangelogSection(readFileIfExists(path.join(cwd, 'CHANGELOG.md')) ?? '', targetVersion.version);
|
|
60
|
+
const metadataAlreadyPrepared = packageJsonUpdatePlan.plan.currentVersion === targetVersion.version && existingChangelogSection !== undefined && tagAvailability.existingTagAtHead;
|
|
61
|
+
return {
|
|
62
|
+
status: 'planned',
|
|
63
|
+
plan: {
|
|
64
|
+
range: releaseRange.range,
|
|
65
|
+
targetVersion: targetVersion.version,
|
|
66
|
+
tagName,
|
|
67
|
+
suggestedBump,
|
|
68
|
+
commitCount: gitHistory.commits.length,
|
|
69
|
+
otp: command.otp,
|
|
70
|
+
hasOtp: command.otp !== undefined,
|
|
71
|
+
metadataAlreadyPrepared,
|
|
72
|
+
existingTagAtHead: tagAvailability.existingTagAtHead,
|
|
73
|
+
metadata: buildReleaseMetadataPlan({
|
|
74
|
+
changelog: {
|
|
75
|
+
path: 'CHANGELOG.md',
|
|
76
|
+
targetVersion: targetVersion.version,
|
|
77
|
+
targetDate,
|
|
78
|
+
replacingExistingSection: changelogUpdatePlan.replacingExistingSection,
|
|
79
|
+
nextContent: changelogUpdatePlan.content,
|
|
80
|
+
},
|
|
81
|
+
packageJson: packageJsonUpdatePlan.plan,
|
|
82
|
+
}),
|
|
83
|
+
warnings: changelogProposal.warnings.map((warning) => ({ code: warning.code, message: warning.message })),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function previewAndConfirmMaintainerRelease(command, ports, cwd = process.cwd()) {
|
|
88
|
+
ports.print('Starting maintainer release planning...\n');
|
|
89
|
+
const planningResult = planMaintainerRelease(command, cwd);
|
|
90
|
+
if (planningResult.status === 'blocked') {
|
|
91
|
+
ports.print(`Release planning blocked: ${planningResult.message}\n`);
|
|
92
|
+
printWarnings(planningResult.warnings, ports);
|
|
93
|
+
return planningResult;
|
|
94
|
+
}
|
|
95
|
+
ports.print(`Release plan ready for ${planningResult.plan.targetVersion} from ${planningResult.plan.range.fromRef} to ${planningResult.plan.range.toRef}.\n\n`);
|
|
96
|
+
const preview = renderReleasePlanPreview(planningResult.plan);
|
|
97
|
+
ports.print(preview);
|
|
98
|
+
if (command.dryRun) {
|
|
99
|
+
ports.print('Dry run requested; no files, commands, tags, publishes, pushes, or GitHub Releases will be created.\n');
|
|
100
|
+
return {
|
|
101
|
+
status: 'dry-run',
|
|
102
|
+
plan: planningResult.plan,
|
|
103
|
+
preview,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!command.assumeYes) {
|
|
107
|
+
ports.print('Waiting for maintainer confirmation before making changes...\n');
|
|
108
|
+
const confirmed = await ports.confirmRelease(planningResult.plan);
|
|
109
|
+
if (!confirmed) {
|
|
110
|
+
ports.print('Release confirmation declined; no changes were written.\n');
|
|
111
|
+
return {
|
|
112
|
+
status: 'declined',
|
|
113
|
+
plan: planningResult.plan,
|
|
114
|
+
preview,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
ports.print('--yes provided; continuing after preview without prompting.\n');
|
|
120
|
+
}
|
|
121
|
+
ports.print('Release confirmed; starting execution.\n');
|
|
122
|
+
return {
|
|
123
|
+
status: 'confirmed',
|
|
124
|
+
plan: planningResult.plan,
|
|
125
|
+
preview,
|
|
126
|
+
execution: await executeConfirmedRelease(planningResult.plan, ports),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function executeConfirmedRelease(plan, ports) {
|
|
130
|
+
const completedSteps = [];
|
|
131
|
+
if (!plan.metadata) {
|
|
132
|
+
return failExecution(completedSteps, 'write-changelog', 'Release plan does not include metadata updates.');
|
|
133
|
+
}
|
|
134
|
+
const npmAuth = await runStep(ports, completedSteps, 'check-npm-auth', 'Checking npm authentication: npm whoami...', 'npm', ['whoami'], 'npm authentication check passed.');
|
|
135
|
+
if (npmAuth.status === 'failed') {
|
|
136
|
+
return npmAuth;
|
|
137
|
+
}
|
|
138
|
+
const githubAuth = await runStep(ports, completedSteps, 'check-github-auth', 'Checking GitHub authentication: gh auth status...', 'gh', ['auth', 'status'], 'GitHub authentication check passed.');
|
|
139
|
+
if (githubAuth.status === 'failed') {
|
|
140
|
+
return githubAuth;
|
|
141
|
+
}
|
|
142
|
+
const build = await runStep(ports, completedSteps, 'build-release', 'Building release artifacts: pnpm build...', 'pnpm', ['build'], 'Release build passed.');
|
|
143
|
+
if (build.status === 'failed') {
|
|
144
|
+
return build;
|
|
145
|
+
}
|
|
146
|
+
if (plan.metadataAlreadyPrepared) {
|
|
147
|
+
completedSteps.push({ name: 'write-changelog', message: `${plan.metadata.changelog.path} already prepared for ${plan.targetVersion}; skipped write.` });
|
|
148
|
+
completedSteps.push({ name: 'write-package-json', message: `${plan.metadata.packageJson.path} already prepared for ${plan.targetVersion}; skipped write.` });
|
|
149
|
+
printExecutionProgress(ports, `Metadata already prepared for ${plan.targetVersion}; skipping changelog and package.json writes.`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
printExecutionProgress(ports, `Writing changelog update to ${plan.metadata.changelog.path}...`);
|
|
153
|
+
ports.writeFile(plan.metadata.changelog.path, plan.metadata.changelog.nextContent);
|
|
154
|
+
completedSteps.push({ name: 'write-changelog', message: `Wrote ${plan.metadata.changelog.path}.` });
|
|
155
|
+
printExecutionProgress(ports, `Done: wrote ${plan.metadata.changelog.path}.`);
|
|
156
|
+
printExecutionProgress(ports, `Writing package version ${plan.metadata.packageJson.targetVersion} to ${plan.metadata.packageJson.path}...`);
|
|
157
|
+
ports.writeFile(plan.metadata.packageJson.path, plan.metadata.packageJson.nextContent);
|
|
158
|
+
completedSteps.push({ name: 'write-package-json', message: `Wrote ${plan.metadata.packageJson.path}.` });
|
|
159
|
+
printExecutionProgress(ports, `Done: wrote ${plan.metadata.packageJson.path}.`);
|
|
160
|
+
}
|
|
161
|
+
printExecutionProgress(ports, 'Running release validation: pnpm run validate:release-publish...');
|
|
162
|
+
const validation = await ports.run('pnpm', ['run', 'validate:release-publish']);
|
|
163
|
+
if (validation.exitCode !== 0) {
|
|
164
|
+
const failure = failExecution(completedSteps, 'validate-release', 'Release validation failed.', validation);
|
|
165
|
+
printExecutionFailure(ports, failure);
|
|
166
|
+
return failure;
|
|
167
|
+
}
|
|
168
|
+
completedSteps.push({ name: 'validate-release', message: 'Release validation passed.' });
|
|
169
|
+
printExecutionProgress(ports, 'Done: release validation passed.');
|
|
170
|
+
const releaseCommitMessage = `chore(release): ${plan.targetVersion}`;
|
|
171
|
+
if (plan.metadataAlreadyPrepared) {
|
|
172
|
+
completedSteps.push({ name: 'create-release-commit', message: `Release commit already exists at HEAD for ${plan.targetVersion}; skipped commit.` });
|
|
173
|
+
printExecutionProgress(ports, `Release commit already exists at HEAD for ${plan.targetVersion}; skipping commit.`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
printExecutionProgress(ports, 'Staging release metadata: git add CHANGELOG.md package.json...');
|
|
177
|
+
const gitAdd = await ports.run('git', ['add', 'CHANGELOG.md', 'package.json']);
|
|
178
|
+
if (gitAdd.exitCode !== 0) {
|
|
179
|
+
const failure = failExecution(completedSteps, 'create-release-commit', 'Staging release metadata failed.', gitAdd);
|
|
180
|
+
printExecutionFailure(ports, failure);
|
|
181
|
+
return failure;
|
|
182
|
+
}
|
|
183
|
+
printExecutionProgress(ports, `Creating release commit: ${releaseCommitMessage}...`);
|
|
184
|
+
const gitCommit = await ports.run('git', ['commit', '-m', releaseCommitMessage]);
|
|
185
|
+
if (gitCommit.exitCode !== 0) {
|
|
186
|
+
const failure = failExecution(completedSteps, 'create-release-commit', 'Creating the release commit failed.', gitCommit);
|
|
187
|
+
printExecutionFailure(ports, failure);
|
|
188
|
+
return failure;
|
|
189
|
+
}
|
|
190
|
+
completedSteps.push({ name: 'create-release-commit', message: `Created release commit: ${releaseCommitMessage}.` });
|
|
191
|
+
printExecutionProgress(ports, `Done: created release commit ${releaseCommitMessage}.`);
|
|
192
|
+
}
|
|
193
|
+
if (plan.existingTagAtHead) {
|
|
194
|
+
completedSteps.push({ name: 'create-release-tag', message: `Release tag ${plan.tagName} already points at HEAD; skipped tag creation.` });
|
|
195
|
+
printExecutionProgress(ports, `Release tag ${plan.tagName} already points at HEAD; skipping tag creation.`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
printExecutionProgress(ports, `Creating release tag: ${plan.tagName}...`);
|
|
199
|
+
const gitTag = await ports.run('git', ['tag', plan.tagName]);
|
|
200
|
+
if (gitTag.exitCode !== 0) {
|
|
201
|
+
const failure = failExecution(completedSteps, 'create-release-tag', `Creating release tag ${plan.tagName} failed.`, gitTag);
|
|
202
|
+
printExecutionFailure(ports, failure);
|
|
203
|
+
return failure;
|
|
204
|
+
}
|
|
205
|
+
completedSteps.push({ name: 'create-release-tag', message: `Created release tag: ${plan.tagName}.` });
|
|
206
|
+
printExecutionProgress(ports, `Done: created release tag ${plan.tagName}.`);
|
|
207
|
+
}
|
|
208
|
+
const npmPublishArgs = buildNpmPublishArgs(plan);
|
|
209
|
+
const npmPublishLog = plan.hasOtp ? 'npm publish --access public --otp ******' : 'npm publish --access public';
|
|
210
|
+
printExecutionProgress(ports, `Publishing package to npm: ${npmPublishLog}...`);
|
|
211
|
+
const npmPublish = await ports.run('npm', npmPublishArgs);
|
|
212
|
+
if (npmPublish.exitCode !== 0) {
|
|
213
|
+
const failure = failExecution(completedSteps, 'publish-npm', 'Publishing to npm failed.', npmPublish);
|
|
214
|
+
printExecutionFailure(ports, failure);
|
|
215
|
+
return failure;
|
|
216
|
+
}
|
|
217
|
+
completedSteps.push({ name: 'publish-npm', message: 'Published package to npm.' });
|
|
218
|
+
printExecutionProgress(ports, 'Done: published package to npm.');
|
|
219
|
+
printExecutionProgress(ports, 'Pushing release commit: git push origin HEAD...');
|
|
220
|
+
const pushCommit = await ports.run('git', ['push', 'origin', 'HEAD']);
|
|
221
|
+
if (pushCommit.exitCode !== 0) {
|
|
222
|
+
const failure = failExecution(completedSteps, 'push-commit', 'Pushing the release commit failed.', pushCommit);
|
|
223
|
+
printExecutionFailure(ports, failure);
|
|
224
|
+
return failure;
|
|
225
|
+
}
|
|
226
|
+
completedSteps.push({ name: 'push-commit', message: 'Pushed release commit to origin.' });
|
|
227
|
+
printExecutionProgress(ports, 'Done: pushed release commit to origin.');
|
|
228
|
+
printExecutionProgress(ports, `Pushing release tag: git push origin ${plan.tagName}...`);
|
|
229
|
+
const pushTag = await ports.run('git', ['push', 'origin', plan.tagName]);
|
|
230
|
+
if (pushTag.exitCode !== 0) {
|
|
231
|
+
const failure = failExecution(completedSteps, 'push-tag', `Pushing release tag ${plan.tagName} failed.`, pushTag);
|
|
232
|
+
printExecutionFailure(ports, failure);
|
|
233
|
+
return failure;
|
|
234
|
+
}
|
|
235
|
+
completedSteps.push({ name: 'push-tag', message: `Pushed release tag ${plan.tagName} to origin.` });
|
|
236
|
+
printExecutionProgress(ports, `Done: pushed release tag ${plan.tagName} to origin.`);
|
|
237
|
+
const releaseBody = extractReleaseBody(plan);
|
|
238
|
+
printExecutionProgress(ports, `Creating GitHub Release: gh release create ${plan.tagName}...`);
|
|
239
|
+
const githubRelease = await ports.run('gh', ['release', 'create', plan.tagName, '--title', plan.tagName, '--notes', releaseBody]);
|
|
240
|
+
if (githubRelease.exitCode !== 0) {
|
|
241
|
+
const failure = failExecution(completedSteps, 'create-github-release', 'Creating the GitHub Release failed.', githubRelease);
|
|
242
|
+
printExecutionFailure(ports, failure);
|
|
243
|
+
return failure;
|
|
244
|
+
}
|
|
245
|
+
completedSteps.push({ name: 'create-github-release', message: `Created GitHub Release ${plan.tagName}.` });
|
|
246
|
+
printExecutionProgress(ports, `Done: created GitHub Release ${plan.tagName}.`);
|
|
247
|
+
return {
|
|
248
|
+
status: 'executed',
|
|
249
|
+
completedSteps,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function printWarnings(warnings, ports) {
|
|
253
|
+
if (warnings.length === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
ports.print('Warnings:\n');
|
|
257
|
+
for (const warning of warnings) {
|
|
258
|
+
ports.print(`- [${warning.code}] ${warning.message}\n`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function printExecutionProgress(ports, message) {
|
|
262
|
+
ports.print?.(`${message}\n`);
|
|
263
|
+
}
|
|
264
|
+
function printExecutionFailure(ports, failure) {
|
|
265
|
+
ports.print?.(`Failed: ${failure.failedStep.message}\n`);
|
|
266
|
+
ports.print?.(`Recovery: ${failure.failedStep.recoveryGuidance}\n`);
|
|
267
|
+
}
|
|
268
|
+
function buildNpmPublishArgs(plan) {
|
|
269
|
+
const args = ['publish', '--access', 'public'];
|
|
270
|
+
if (plan.otp) {
|
|
271
|
+
args.push('--otp', plan.otp);
|
|
272
|
+
}
|
|
273
|
+
return args;
|
|
274
|
+
}
|
|
275
|
+
async function runStep(ports, completedSteps, stepName, startMessage, command, args, completedMessage) {
|
|
276
|
+
printExecutionProgress(ports, startMessage);
|
|
277
|
+
const result = await ports.run(command, args);
|
|
278
|
+
if (result.exitCode !== 0) {
|
|
279
|
+
const failure = failExecution(completedSteps, stepName, `${completedMessage.replace(/\.$/, '')} failed.`, result);
|
|
280
|
+
printExecutionFailure(ports, failure);
|
|
281
|
+
return failure;
|
|
282
|
+
}
|
|
283
|
+
completedSteps.push({ name: stepName, message: completedMessage });
|
|
284
|
+
printExecutionProgress(ports, `Done: ${completedMessage}`);
|
|
285
|
+
return { status: 'ok' };
|
|
286
|
+
}
|
|
287
|
+
function resolveTargetVersion(input) {
|
|
288
|
+
if (input.requestedVersion) {
|
|
289
|
+
const requestedVersion = parseSemverVersion(input.requestedVersion);
|
|
290
|
+
if (requestedVersion.status === 'invalid') {
|
|
291
|
+
return blockedTargetVersion('invalid-version', requestedVersion.message);
|
|
292
|
+
}
|
|
293
|
+
return { status: 'ok', version: requestedVersion.version.version };
|
|
294
|
+
}
|
|
295
|
+
const previousVersion = parseSemverVersion(input.fromRef);
|
|
296
|
+
if (previousVersion.status === 'invalid') {
|
|
297
|
+
return blockedTargetVersion('invalid-version', `Release baseline \`${input.fromRef}\` must be SemVer-like when --version is omitted.`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
status: 'ok',
|
|
301
|
+
version: incrementSemverVersion(previousVersion.version, input.suggestedBump).version,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function blocked(code, message) {
|
|
305
|
+
return {
|
|
306
|
+
status: 'blocked',
|
|
307
|
+
message,
|
|
308
|
+
warnings: [{ code, message }],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function blockedTargetVersion(code, message) {
|
|
312
|
+
return {
|
|
313
|
+
status: 'blocked',
|
|
314
|
+
message,
|
|
315
|
+
warnings: [{ code, message }],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function readFileIfExists(filePath) {
|
|
319
|
+
if (!fs.existsSync(filePath)) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
323
|
+
}
|
|
324
|
+
function failExecution(completedSteps, name, message, commandResult) {
|
|
325
|
+
const stderr = commandResult?.stderr?.trim();
|
|
326
|
+
const commandContext = stderr ? ` ${stderr}` : '';
|
|
327
|
+
return {
|
|
328
|
+
status: 'failed',
|
|
329
|
+
completedSteps,
|
|
330
|
+
failedStep: {
|
|
331
|
+
name,
|
|
332
|
+
message: `${message}${commandContext}`,
|
|
333
|
+
recoveryGuidance: buildRecoveryGuidance(name),
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function buildRecoveryGuidance(name) {
|
|
338
|
+
switch (name) {
|
|
339
|
+
case 'validate-release':
|
|
340
|
+
return 'Fix validation failures, restore or review local release metadata changes, then rerun the release workflow.';
|
|
341
|
+
case 'check-npm-auth':
|
|
342
|
+
return 'Run `npm login` with an account that can publish this package, then rerun the release workflow.';
|
|
343
|
+
case 'check-github-auth':
|
|
344
|
+
return 'Run `gh auth login` or fix GitHub CLI authentication, then rerun the release workflow.';
|
|
345
|
+
case 'build-release':
|
|
346
|
+
return 'Fix the build failure, then rerun the release workflow.';
|
|
347
|
+
case 'create-release-commit':
|
|
348
|
+
return 'Review local release metadata changes, resolve git commit issues, then rerun or complete the release manually.';
|
|
349
|
+
case 'create-release-tag':
|
|
350
|
+
return 'Review the release commit and tag state, then create the tag manually or rerun after cleanup.';
|
|
351
|
+
case 'publish-npm':
|
|
352
|
+
return 'Review npm authentication/package state. If the package was not published, rerun after fixing npm publish. The workflow publishes with `npm publish --access public`.';
|
|
353
|
+
case 'write-changelog':
|
|
354
|
+
case 'write-package-json':
|
|
355
|
+
return 'Review local file permissions and release metadata, then rerun the release workflow.';
|
|
356
|
+
case 'push-commit':
|
|
357
|
+
return 'npm publish may have completed. Push the release commit manually after resolving git remote issues.';
|
|
358
|
+
case 'push-tag':
|
|
359
|
+
return 'npm publish and the release commit push may have completed. Push the release tag manually after resolving git remote issues.';
|
|
360
|
+
case 'create-github-release':
|
|
361
|
+
return 'npm publish and git push may have completed. Create the GitHub Release manually with the finalized CHANGELOG.md section.';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function extractReleaseBody(plan) {
|
|
365
|
+
if (!plan.metadata) {
|
|
366
|
+
return `Release ${plan.targetVersion}`;
|
|
367
|
+
}
|
|
368
|
+
return extractReleaseChangelogSection(plan.metadata.changelog.nextContent, plan.targetVersion) ?? `Release ${plan.targetVersion}`;
|
|
369
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export function planPackageJsonVersionUpdate(existingContent, targetVersion) {
|
|
2
|
+
if (existingContent === undefined) {
|
|
3
|
+
return malformedPackageJson('Root package.json could not be read.');
|
|
4
|
+
}
|
|
5
|
+
const parsedPackageJson = parsePackageJson(existingContent);
|
|
6
|
+
if (parsedPackageJson.status === 'blocked') {
|
|
7
|
+
return parsedPackageJson;
|
|
8
|
+
}
|
|
9
|
+
const currentVersion = parsedPackageJson.value.version;
|
|
10
|
+
if (typeof currentVersion !== 'string') {
|
|
11
|
+
return malformedPackageJson('Root package.json must contain a string version field.');
|
|
12
|
+
}
|
|
13
|
+
const versionValueRange = findRootStringPropertyValueRange(existingContent, 'version');
|
|
14
|
+
if (!versionValueRange) {
|
|
15
|
+
return malformedPackageJson('Root package.json version field could not be located safely.');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
status: 'ok',
|
|
19
|
+
plan: {
|
|
20
|
+
path: 'package.json',
|
|
21
|
+
currentVersion,
|
|
22
|
+
targetVersion,
|
|
23
|
+
nextContent: replaceRange(existingContent, versionValueRange.start, versionValueRange.end, JSON.stringify(targetVersion)),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parsePackageJson(content) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(content);
|
|
30
|
+
if (!isRecord(parsed)) {
|
|
31
|
+
return malformedPackageJson('Root package.json must contain a JSON object.');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
status: 'ok',
|
|
35
|
+
value: parsed,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return malformedPackageJson('Root package.json could not be parsed as JSON.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function findRootStringPropertyValueRange(content, propertyName) {
|
|
43
|
+
let index = skipWhitespace(content, 0);
|
|
44
|
+
if (content[index] !== '{') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
index += 1;
|
|
48
|
+
while (index < content.length) {
|
|
49
|
+
index = skipWhitespaceAndComma(content, index);
|
|
50
|
+
if (content[index] === '}') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (content[index] !== '"') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const keyEnd = findStringLiteralEnd(content, index);
|
|
57
|
+
if (keyEnd === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const key = parseStringLiteral(content.slice(index, keyEnd));
|
|
61
|
+
index = skipWhitespace(content, keyEnd);
|
|
62
|
+
if (content[index] !== ':') {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
index = skipWhitespace(content, index + 1);
|
|
66
|
+
const valueStart = index;
|
|
67
|
+
const valueEnd = findJsonValueEnd(content, valueStart);
|
|
68
|
+
if (valueEnd === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (key === propertyName) {
|
|
72
|
+
if (content[valueStart] !== '"') {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
start: valueStart,
|
|
77
|
+
end: valueEnd,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
index = valueEnd;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
function findJsonValueEnd(content, start) {
|
|
85
|
+
const firstCharacter = content[start];
|
|
86
|
+
if (firstCharacter === '"') {
|
|
87
|
+
return findStringLiteralEnd(content, start);
|
|
88
|
+
}
|
|
89
|
+
if (firstCharacter === '{' || firstCharacter === '[') {
|
|
90
|
+
return findContainerEnd(content, start);
|
|
91
|
+
}
|
|
92
|
+
const match = /^[^,\]}]+/.exec(content.slice(start));
|
|
93
|
+
return match ? start + match[0].trimEnd().length : undefined;
|
|
94
|
+
}
|
|
95
|
+
function findContainerEnd(content, start) {
|
|
96
|
+
const openingCharacter = content[start];
|
|
97
|
+
const closingCharacter = openingCharacter === '{' ? '}' : ']';
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let index = start;
|
|
100
|
+
while (index < content.length) {
|
|
101
|
+
const character = content[index];
|
|
102
|
+
if (character === '"') {
|
|
103
|
+
const stringEnd = findStringLiteralEnd(content, index);
|
|
104
|
+
if (stringEnd === undefined) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
index = stringEnd;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (character === openingCharacter) {
|
|
111
|
+
depth += 1;
|
|
112
|
+
}
|
|
113
|
+
if (character === closingCharacter) {
|
|
114
|
+
depth -= 1;
|
|
115
|
+
if (depth === 0) {
|
|
116
|
+
return index + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
index += 1;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
function findStringLiteralEnd(content, start) {
|
|
124
|
+
let escaped = false;
|
|
125
|
+
for (let index = start + 1; index < content.length; index += 1) {
|
|
126
|
+
const character = content[index];
|
|
127
|
+
if (escaped) {
|
|
128
|
+
escaped = false;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (character === '\\') {
|
|
132
|
+
escaped = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === '"') {
|
|
136
|
+
return index + 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
function parseStringLiteral(value) {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(value);
|
|
144
|
+
return typeof parsed === 'string' ? parsed : undefined;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function skipWhitespace(content, start) {
|
|
151
|
+
let index = start;
|
|
152
|
+
while (/\s/.test(content[index] ?? '')) {
|
|
153
|
+
index += 1;
|
|
154
|
+
}
|
|
155
|
+
return index;
|
|
156
|
+
}
|
|
157
|
+
function skipWhitespaceAndComma(content, start) {
|
|
158
|
+
let index = skipWhitespace(content, start);
|
|
159
|
+
if (content[index] === ',') {
|
|
160
|
+
index = skipWhitespace(content, index + 1);
|
|
161
|
+
}
|
|
162
|
+
return index;
|
|
163
|
+
}
|
|
164
|
+
function replaceRange(content, start, end, replacement) {
|
|
165
|
+
return `${content.slice(0, start)}${replacement}${content.slice(end)}`;
|
|
166
|
+
}
|
|
167
|
+
function isRecord(value) {
|
|
168
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
169
|
+
}
|
|
170
|
+
function malformedPackageJson(message) {
|
|
171
|
+
return {
|
|
172
|
+
status: 'blocked',
|
|
173
|
+
message,
|
|
174
|
+
warnings: [
|
|
175
|
+
{
|
|
176
|
+
code: 'malformed-package-json',
|
|
177
|
+
message,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function incrementSemverVersion(version, bump) {
|
|
2
|
+
switch (bump) {
|
|
3
|
+
case 'major':
|
|
4
|
+
return buildParsedVersion(version.major + 1, 0, 0);
|
|
5
|
+
case 'minor':
|
|
6
|
+
return buildParsedVersion(version.major, version.minor + 1, 0);
|
|
7
|
+
case 'patch':
|
|
8
|
+
return buildParsedVersion(version.major, version.minor, version.patch + 1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function formatReleaseTagName(version) {
|
|
12
|
+
return `v${version}`;
|
|
13
|
+
}
|
|
14
|
+
export function buildReleaseMetadataPlan(metadata) {
|
|
15
|
+
return metadata;
|
|
16
|
+
}
|
|
17
|
+
export function deriveSuggestedBumpFromChangelogProposal(proposal) {
|
|
18
|
+
return proposal.semverGuidance.suggestedBump;
|
|
19
|
+
}
|
|
20
|
+
export function renderReleasePlanPreview(plan) {
|
|
21
|
+
const lines = [
|
|
22
|
+
'Release plan preview',
|
|
23
|
+
'',
|
|
24
|
+
`Git range: ${plan.range.fromRef}..${plan.range.toRef}`,
|
|
25
|
+
`Proposed version: ${plan.targetVersion}`,
|
|
26
|
+
`Suggested SemVer bump: ${plan.suggestedBump}`,
|
|
27
|
+
`Commits inspected: ${plan.commitCount}`,
|
|
28
|
+
'',
|
|
29
|
+
'Planned local metadata updates:',
|
|
30
|
+
...renderMetadataPreview(plan),
|
|
31
|
+
'',
|
|
32
|
+
'Planned release actions after confirmation:',
|
|
33
|
+
'- Check npm authentication: npm whoami',
|
|
34
|
+
'- Check GitHub authentication: gh auth status',
|
|
35
|
+
'- Build release artifacts: pnpm build',
|
|
36
|
+
`- Run validation: pnpm run validate:release-publish`,
|
|
37
|
+
`- Create release commit: chore(release): ${plan.targetVersion}`,
|
|
38
|
+
`- Create git tag: ${plan.tagName}`,
|
|
39
|
+
`- Publish package: npm publish --access public${plan.hasOtp ? ' --otp ******' : ''}`,
|
|
40
|
+
`- Push release commit: git push origin HEAD`,
|
|
41
|
+
`- Push release tag: git push origin ${plan.tagName}`,
|
|
42
|
+
`- Create GitHub Release: gh release create ${plan.tagName}`,
|
|
43
|
+
];
|
|
44
|
+
if (plan.warnings.length > 0) {
|
|
45
|
+
lines.push('', 'Warnings:', ...plan.warnings.map(formatReleaseWarning));
|
|
46
|
+
}
|
|
47
|
+
return `${lines.join('\n')}\n`;
|
|
48
|
+
}
|
|
49
|
+
export function extractReleaseChangelogSection(changelogContent, targetVersion) {
|
|
50
|
+
const sectionStart = changelogContent.indexOf(`## ${targetVersion} - `);
|
|
51
|
+
if (sectionStart < 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const nextSectionStart = changelogContent.indexOf('\n## ', sectionStart + 1);
|
|
55
|
+
const sectionEnd = nextSectionStart >= 0 ? nextSectionStart : changelogContent.length;
|
|
56
|
+
return changelogContent.slice(sectionStart, sectionEnd).trim();
|
|
57
|
+
}
|
|
58
|
+
function buildParsedVersion(major, minor, patch) {
|
|
59
|
+
return {
|
|
60
|
+
version: `${major}.${minor}.${patch}`,
|
|
61
|
+
major,
|
|
62
|
+
minor,
|
|
63
|
+
patch,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function renderMetadataPreview(plan) {
|
|
67
|
+
if (!plan.metadata) {
|
|
68
|
+
return ['- No local metadata updates planned.'];
|
|
69
|
+
}
|
|
70
|
+
const changelogAction = plan.metadata.changelog.replacingExistingSection ? 'Replace' : 'Create';
|
|
71
|
+
return [
|
|
72
|
+
`- ${changelogAction} CHANGELOG.md section: ${plan.metadata.changelog.targetVersion} - ${plan.metadata.changelog.targetDate}`,
|
|
73
|
+
...renderChangelogSectionPreview(plan.metadata.changelog.nextContent, plan.metadata.changelog.targetVersion),
|
|
74
|
+
`- Update package.json version: ${plan.metadata.packageJson.currentVersion} -> ${plan.metadata.packageJson.targetVersion}`,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function renderChangelogSectionPreview(changelogContent, targetVersion) {
|
|
78
|
+
const section = extractChangelogVersionSection(changelogContent, targetVersion);
|
|
79
|
+
const previewLines = section.split('\n').slice(0, 20);
|
|
80
|
+
return [' Changelog preview:', ...previewLines.map((line) => ` ${line}`)];
|
|
81
|
+
}
|
|
82
|
+
function extractChangelogVersionSection(changelogContent, targetVersion) {
|
|
83
|
+
return extractReleaseChangelogSection(changelogContent, targetVersion) ?? '(target changelog section not found in planned content)';
|
|
84
|
+
}
|
|
85
|
+
function formatReleaseWarning(warning) {
|
|
86
|
+
return `- [${warning.code}] ${warning.message}`;
|
|
87
|
+
}
|
|
@@ -4,9 +4,9 @@ import { intro, log, outro } from '@clack/prompts';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
6
6
|
import { getProjectContext } from '../../shared/paths.js';
|
|
7
|
-
import { askForArchitectureSkill, askForFrameworkSkills, askForLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro } from '
|
|
8
|
-
import { readStateForDoctor } from '
|
|
9
|
-
import { getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '
|
|
7
|
+
import { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro } from '../interactive-guidance/index.js';
|
|
8
|
+
import { readStateForDoctor } from '../workflow-state-registry/index.js';
|
|
9
|
+
import { getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../workflow-target-planning/index.js';
|
|
10
10
|
export async function handleInitProject(_command) {
|
|
11
11
|
await renderIntro();
|
|
12
12
|
intro(chalk.cyan('Setting up Sibu'));
|
|
@@ -30,9 +30,10 @@ export async function handleInitProject(_command) {
|
|
|
30
30
|
const selectedAgents = await askForSupportedAgents();
|
|
31
31
|
const selectedLanguageSkills = await askForLanguageSkills();
|
|
32
32
|
const selectedFrameworkSkills = await askForFrameworkSkills();
|
|
33
|
+
const selectedDatabaseSkills = await askForDatabaseSkills();
|
|
33
34
|
const selectedArchitectureSkill = await askForArchitectureSkill();
|
|
34
35
|
const selectedWorkflowSkills = await askForWorkflowSkills();
|
|
35
|
-
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills);
|
|
36
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills);
|
|
36
37
|
const missingTargets = targets.filter((target) => !fs.existsSync(target.targetPath));
|
|
37
38
|
log.message('I will create this project’s initial AI workflow files.');
|
|
38
39
|
for (const target of targets) {
|
|
@@ -53,13 +54,14 @@ export async function handleInitProject(_command) {
|
|
|
53
54
|
selectedFrameworkSkills,
|
|
54
55
|
selectedArchitectureSkill,
|
|
55
56
|
selectedWorkflowSkills,
|
|
57
|
+
selectedDatabaseSkills,
|
|
56
58
|
});
|
|
57
59
|
for (const file of files) {
|
|
58
60
|
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
59
61
|
fs.writeFileSync(file.targetPath, file.contents, { encoding: 'utf8', flag: 'wx' });
|
|
60
62
|
log.success(`Created ${file.label}`);
|
|
61
63
|
}
|
|
62
|
-
writeSibuState({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, targets });
|
|
64
|
+
writeSibuState({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, targets });
|
|
63
65
|
log.success(`Created ${STATE_RELATIVE_PATH}`);
|
|
64
66
|
outro(chalk.green('Setup complete.'));
|
|
65
67
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handleInitProject } from './handler.js';
|