musubi-sdd 5.8.2 → 5.9.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.
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MUSUBI Release CLI
5
+ *
6
+ * Automate release process including:
7
+ * - Version bumping
8
+ * - CHANGELOG generation
9
+ * - Git tagging
10
+ * - npm publishing
11
+ *
12
+ * Usage:
13
+ * musubi-release patch|minor|major [options]
14
+ * musubi-release changelog
15
+ * musubi-release notes
16
+ */
17
+
18
+ const { program } = require('commander');
19
+ const { execSync } = require('child_process');
20
+ const fs = require('fs-extra');
21
+ const path = require('path');
22
+ const chalk = require('chalk');
23
+ const { ChangelogGenerator } = require('../src/generators/changelog-generator');
24
+
25
+ const projectRoot = process.cwd();
26
+
27
+ /**
28
+ * Read package.json
29
+ */
30
+ async function readPackageJson() {
31
+ const packagePath = path.join(projectRoot, 'package.json');
32
+ if (await fs.pathExists(packagePath)) {
33
+ return JSON.parse(await fs.readFile(packagePath, 'utf8'));
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Write package.json
40
+ */
41
+ async function writePackageJson(pkg) {
42
+ const packagePath = path.join(projectRoot, 'package.json');
43
+ await fs.writeFile(packagePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
44
+ }
45
+
46
+ /**
47
+ * Get current version
48
+ */
49
+ async function getCurrentVersion() {
50
+ const pkg = await readPackageJson();
51
+ return pkg?.version || '0.0.0';
52
+ }
53
+
54
+ /**
55
+ * Bump version
56
+ */
57
+ function bumpVersion(version, type) {
58
+ const parts = version.split('.').map(Number);
59
+
60
+ switch (type) {
61
+ case 'major':
62
+ return `${parts[0] + 1}.0.0`;
63
+ case 'minor':
64
+ return `${parts[0]}.${parts[1] + 1}.0`;
65
+ case 'patch':
66
+ return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
67
+ default:
68
+ // If type is a specific version, return it
69
+ if (/^\d+\.\d+\.\d+/.test(type)) {
70
+ return type;
71
+ }
72
+ throw new Error(`Invalid version type: ${type}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Run pre-release checks
78
+ */
79
+ async function preReleaseChecks(options) {
80
+ const checks = [];
81
+
82
+ // Check for uncommitted changes
83
+ if (!options.skipGitCheck) {
84
+ try {
85
+ const status = execSync('git status --porcelain', {
86
+ cwd: projectRoot,
87
+ encoding: 'utf8'
88
+ });
89
+ if (status.trim()) {
90
+ checks.push({
91
+ name: 'Uncommitted changes',
92
+ passed: false,
93
+ message: 'You have uncommitted changes'
94
+ });
95
+ } else {
96
+ checks.push({ name: 'Git status', passed: true });
97
+ }
98
+ } catch {
99
+ checks.push({ name: 'Git status', passed: false, message: 'Git not available' });
100
+ }
101
+ }
102
+
103
+ // Run tests
104
+ if (!options.skipTests) {
105
+ try {
106
+ console.log(chalk.gray(' Running tests...'));
107
+ execSync('npm test', { cwd: projectRoot, stdio: 'pipe' });
108
+ checks.push({ name: 'Tests', passed: true });
109
+ } catch {
110
+ checks.push({ name: 'Tests', passed: false, message: 'Tests failed' });
111
+ }
112
+ }
113
+
114
+ // Check coverage (if available)
115
+ if (!options.skipCoverage) {
116
+ try {
117
+ const coveragePath = path.join(projectRoot, 'coverage/coverage-summary.json');
118
+ if (await fs.pathExists(coveragePath)) {
119
+ const coverage = JSON.parse(await fs.readFile(coveragePath, 'utf8'));
120
+ const totalCoverage = coverage.total?.lines?.pct || 0;
121
+ const threshold = options.coverageThreshold || 70;
122
+
123
+ if (totalCoverage >= threshold) {
124
+ checks.push({
125
+ name: 'Coverage',
126
+ passed: true,
127
+ message: `${totalCoverage}% >= ${threshold}%`
128
+ });
129
+ } else {
130
+ checks.push({
131
+ name: 'Coverage',
132
+ passed: false,
133
+ message: `${totalCoverage}% < ${threshold}%`
134
+ });
135
+ }
136
+ }
137
+ } catch {
138
+ // Coverage check optional
139
+ }
140
+ }
141
+
142
+ return checks;
143
+ }
144
+
145
+ /**
146
+ * Create git tag
147
+ */
148
+ function createGitTag(version, message) {
149
+ try {
150
+ execSync(`git tag -a v${version} -m "${message}"`, {
151
+ cwd: projectRoot,
152
+ encoding: 'utf8'
153
+ });
154
+ return true;
155
+ } catch (error) {
156
+ console.error(chalk.yellow(`Warning: Could not create tag: ${error.message}`));
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Push to remote
163
+ */
164
+ function pushToRemote(includeTags = true) {
165
+ try {
166
+ if (includeTags) {
167
+ execSync('git push --follow-tags', { cwd: projectRoot, encoding: 'utf8' });
168
+ } else {
169
+ execSync('git push', { cwd: projectRoot, encoding: 'utf8' });
170
+ }
171
+ return true;
172
+ } catch (error) {
173
+ console.error(chalk.yellow(`Warning: Could not push: ${error.message}`));
174
+ return false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Publish to npm
180
+ */
181
+ function publishToNpm(options = {}) {
182
+ try {
183
+ let cmd = 'npm publish';
184
+ if (options.access) cmd += ` --access ${options.access}`;
185
+ if (options.tag) cmd += ` --tag ${options.tag}`;
186
+ if (options.dryRun) cmd += ' --dry-run';
187
+
188
+ execSync(cmd, { cwd: projectRoot, stdio: 'inherit' });
189
+ return true;
190
+ } catch (error) {
191
+ console.error(chalk.red(`Error publishing: ${error.message}`));
192
+ return false;
193
+ }
194
+ }
195
+
196
+ // CLI Commands
197
+ program
198
+ .name('musubi-release')
199
+ .description('MUSUBI Release Manager - Automate release process')
200
+ .version('1.0.0');
201
+
202
+ program
203
+ .command('bump <type>')
204
+ .description('Bump version and prepare release (type: patch, minor, major, or specific version)')
205
+ .option('--skip-tests', 'Skip running tests')
206
+ .option('--skip-coverage', 'Skip coverage check')
207
+ .option('--skip-git-check', 'Skip git status check')
208
+ .option('--skip-changelog', 'Skip CHANGELOG update')
209
+ .option('--no-tag', 'Skip git tag creation')
210
+ .option('--no-push', 'Skip pushing to remote')
211
+ .option('--dry-run', 'Show what would be done without making changes')
212
+ .action(async (type, options) => {
213
+ try {
214
+ console.log(chalk.bold('\nšŸš€ MUSUBI Release Manager\n'));
215
+
216
+ // Get current version
217
+ const currentVersion = await getCurrentVersion();
218
+ const newVersion = bumpVersion(currentVersion, type);
219
+
220
+ console.log(chalk.white(` Current version: ${chalk.gray(currentVersion)}`));
221
+ console.log(chalk.white(` New version: ${chalk.cyan(newVersion)}`));
222
+ console.log();
223
+
224
+ // Pre-release checks
225
+ console.log(chalk.bold('šŸ“‹ Pre-release checks:\n'));
226
+ const checks = await preReleaseChecks(options);
227
+
228
+ let allPassed = true;
229
+ for (const check of checks) {
230
+ if (check.passed) {
231
+ console.log(chalk.green(` āœ… ${check.name}${check.message ? `: ${check.message}` : ''}`));
232
+ } else {
233
+ console.log(chalk.red(` āŒ ${check.name}: ${check.message}`));
234
+ allPassed = false;
235
+ }
236
+ }
237
+
238
+ if (!allPassed && !options.dryRun) {
239
+ console.log(chalk.red('\nāŒ Pre-release checks failed. Use --skip-* options to bypass.'));
240
+ process.exit(1);
241
+ }
242
+
243
+ if (options.dryRun) {
244
+ console.log(chalk.yellow('\nšŸ” Dry run - no changes made'));
245
+ return;
246
+ }
247
+
248
+ console.log();
249
+
250
+ // Update package.json
251
+ console.log(chalk.white(' šŸ“¦ Updating package.json...'));
252
+ const pkg = await readPackageJson();
253
+ if (pkg) {
254
+ pkg.version = newVersion;
255
+ await writePackageJson(pkg);
256
+ console.log(chalk.green(' Done'));
257
+ }
258
+
259
+ // Update CHANGELOG
260
+ if (!options.skipChangelog) {
261
+ console.log(chalk.white(' šŸ“ Updating CHANGELOG...'));
262
+ const generator = new ChangelogGenerator(projectRoot);
263
+ const result = await generator.update(newVersion);
264
+ console.log(chalk.green(` Added ${result.commitCount} commits`));
265
+ }
266
+
267
+ // Git commit
268
+ console.log(chalk.white(' šŸ’¾ Committing changes...'));
269
+ execSync(`git add -A && git commit -m "chore(release): v${newVersion}"`, {
270
+ cwd: projectRoot,
271
+ encoding: 'utf8',
272
+ });
273
+ console.log(chalk.green(' Done'));
274
+
275
+ // Create tag
276
+ if (options.tag !== false) {
277
+ console.log(chalk.white(' šŸ·ļø Creating git tag...'));
278
+ createGitTag(newVersion, `Release v${newVersion}`);
279
+ console.log(chalk.green(' Done'));
280
+ }
281
+
282
+ // Push
283
+ if (options.push !== false) {
284
+ console.log(chalk.white(' ā¬†ļø Pushing to remote...'));
285
+ pushToRemote(options.tag !== false);
286
+ console.log(chalk.green(' Done'));
287
+ }
288
+
289
+ console.log(chalk.green(`\nāœ… Released v${newVersion}!\n`));
290
+
291
+ } catch (error) {
292
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
293
+ process.exit(1);
294
+ }
295
+ });
296
+
297
+ program
298
+ .command('publish')
299
+ .description('Publish to npm')
300
+ .option('--access <access>', 'Package access level (public/restricted)')
301
+ .option('--tag <tag>', 'npm dist-tag')
302
+ .option('--dry-run', 'Run npm publish --dry-run')
303
+ .action(async (options) => {
304
+ console.log(chalk.bold('\nšŸ“¦ Publishing to npm...\n'));
305
+
306
+ const success = publishToNpm(options);
307
+ if (success) {
308
+ console.log(chalk.green('\nāœ… Published successfully!'));
309
+ } else {
310
+ process.exit(1);
311
+ }
312
+ });
313
+
314
+ program
315
+ .command('changelog')
316
+ .description('Generate or update CHANGELOG')
317
+ .option('-v, --version <version>', 'Version for the changelog entry')
318
+ .option('-f, --from <tag>', 'Starting tag for commit range')
319
+ .action(async (options) => {
320
+ try {
321
+ console.log(chalk.bold('\nšŸ“ Generating CHANGELOG...\n'));
322
+
323
+ const generator = new ChangelogGenerator(projectRoot);
324
+ const version = options.version || (await getCurrentVersion());
325
+ const result = await generator.update(version, options.from);
326
+
327
+ console.log(chalk.white(` Version: ${result.version}`));
328
+ console.log(chalk.white(` Commits: ${result.commitCount}`));
329
+ console.log();
330
+
331
+ // Show category breakdown
332
+ for (const [category, commits] of Object.entries(result.categories)) {
333
+ if (commits.length > 0) {
334
+ console.log(chalk.gray(` ${category}: ${commits.length}`));
335
+ }
336
+ }
337
+
338
+ console.log(chalk.green('\nāœ… CHANGELOG updated!'));
339
+ } catch (error) {
340
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
341
+ process.exit(1);
342
+ }
343
+ });
344
+
345
+ program
346
+ .command('notes')
347
+ .description('Generate release notes')
348
+ .option('-v, --version <version>', 'Version for the release notes')
349
+ .option('-f, --from <tag>', 'Starting tag for commit range')
350
+ .option('-o, --output <file>', 'Output file (default: stdout)')
351
+ .action(async (options) => {
352
+ try {
353
+ const generator = new ChangelogGenerator(projectRoot);
354
+ const version = options.version || (await getCurrentVersion());
355
+ const notes = generator.generateReleaseNotes(version, options.from);
356
+
357
+ if (options.output) {
358
+ await fs.writeFile(options.output, notes, 'utf8');
359
+ console.log(chalk.green(`āœ… Release notes written to ${options.output}`));
360
+ } else {
361
+ console.log(notes);
362
+ }
363
+ } catch (error) {
364
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
365
+ process.exit(1);
366
+ }
367
+ });
368
+
369
+ program
370
+ .command('status')
371
+ .description('Show release status')
372
+ .action(async () => {
373
+ try {
374
+ console.log(chalk.bold('\nšŸ“Š Release Status\n'));
375
+
376
+ const currentVersion = await getCurrentVersion();
377
+ console.log(chalk.white(` Current version: ${chalk.cyan(currentVersion)}`));
378
+
379
+ // Get last tag
380
+ try {
381
+ const lastTag = execSync('git describe --tags --abbrev=0', {
382
+ cwd: projectRoot,
383
+ encoding: 'utf8',
384
+ }).trim();
385
+ console.log(chalk.white(` Last tag: ${chalk.gray(lastTag)}`));
386
+
387
+ // Count commits since last tag
388
+ const commitCount = execSync(`git rev-list ${lastTag}..HEAD --count`, {
389
+ cwd: projectRoot,
390
+ encoding: 'utf8',
391
+ }).trim();
392
+ console.log(chalk.white(` Commits since tag: ${chalk.yellow(commitCount)}`));
393
+ } catch {
394
+ console.log(chalk.gray(' No tags found'));
395
+ }
396
+
397
+ // Check for uncommitted changes
398
+ const status = execSync('git status --porcelain', {
399
+ cwd: projectRoot,
400
+ encoding: 'utf8',
401
+ });
402
+ if (status.trim()) {
403
+ console.log(chalk.yellow(' Uncommitted changes: Yes'));
404
+ } else {
405
+ console.log(chalk.green(' Uncommitted changes: No'));
406
+ }
407
+
408
+ console.log();
409
+ } catch (error) {
410
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
411
+ process.exit(1);
412
+ }
413
+ });
414
+
415
+ program.parse();
@@ -58,16 +58,35 @@ async function showStatus() {
58
58
 
59
59
  console.log(chalk.bold('\nšŸ“Š Workflow Status\n'));
60
60
  console.log(chalk.white(`Feature: ${chalk.cyan(state.feature)}`));
61
+ console.log(chalk.white(`Mode: ${chalk.magenta(state.mode || 'default')} (${getModeDescription(state.mode || 'medium')})`));
61
62
  console.log(chalk.white(`Current Stage: ${formatStage(state.currentStage)}`));
62
63
  console.log(chalk.white(`Started: ${new Date(state.startedAt).toLocaleString()}`));
64
+
65
+ // Show mode config
66
+ if (state.config) {
67
+ console.log(chalk.bold('\nāš™ļø Mode Configuration:'));
68
+ console.log(chalk.gray(` Coverage Threshold: ${state.config.coverageThreshold}%`));
69
+ console.log(chalk.gray(` EARS Required: ${state.config.earsRequired ? 'Yes' : 'No'}`));
70
+ console.log(chalk.gray(` ADR Required: ${state.config.adrRequired ? 'Yes' : 'No'}`));
71
+ if (state.config.skippedArtifacts?.length > 0) {
72
+ console.log(chalk.gray(` Skipped Artifacts: ${state.config.skippedArtifacts.join(', ')}`));
73
+ }
74
+ }
63
75
 
64
- // Show stage progress
76
+ // Show stage progress - use mode-specific stages if available
65
77
  console.log(chalk.bold('\nšŸ“ˆ Stage Progress:\n'));
66
78
 
67
- const allStages = Object.keys(WORKFLOW_STAGES);
68
- const currentIndex = allStages.indexOf(state.currentStage);
79
+ let stagesToShow;
80
+ if (state.mode) {
81
+ const modeManager = engine.getModeManager();
82
+ stagesToShow = await modeManager.getStages(state.mode);
83
+ } else {
84
+ stagesToShow = Object.keys(WORKFLOW_STAGES);
85
+ }
86
+
87
+ const currentIndex = stagesToShow.indexOf(state.currentStage);
69
88
 
70
- allStages.forEach((stage, index) => {
89
+ stagesToShow.forEach((stage, index) => {
71
90
  const data = state.stages[stage];
72
91
  let status = '';
73
92
  let color = chalk.gray;
@@ -180,18 +199,68 @@ program
180
199
  program
181
200
  .command('init <feature>')
182
201
  .description('Initialize a new workflow for a feature')
183
- .option('-s, --stage <stage>', 'Starting stage', 'requirements')
202
+ .option('-s, --stage <stage>', 'Starting stage (auto-detected based on mode)')
203
+ .option('-m, --mode <mode>', 'Workflow mode: small, medium, or large (auto-detected from feature name)')
184
204
  .action(async (feature, options) => {
185
205
  try {
186
- const state = await engine.initWorkflow(feature, { startStage: options.stage });
206
+ const initOptions = {};
207
+ if (options.stage) initOptions.startStage = options.stage;
208
+ if (options.mode) initOptions.mode = options.mode;
209
+
210
+ const state = await engine.initWorkflow(feature, initOptions);
187
211
  console.log(chalk.green(`\nāœ… Workflow initialized for "${feature}"`));
188
- console.log(chalk.cyan(` Starting at: ${formatStage(state.currentStage)}`));
212
+ console.log(chalk.cyan(` Mode: ${state.mode} (${getModeDescription(state.mode)})`));
213
+ console.log(chalk.cyan(` Starting: ${formatStage(state.currentStage)}`));
214
+ console.log(chalk.gray(` Coverage: ${state.config.coverageThreshold}%`));
215
+
216
+ // Show stages for this mode
217
+ const modeManager = engine.getModeManager();
218
+ const stages = await modeManager.getStages(state.mode);
219
+ console.log(chalk.bold('\nšŸ“‹ Workflow stages:'));
220
+ stages.forEach((s, i) => {
221
+ const isCurrent = s === state.currentStage;
222
+ const prefix = isCurrent ? '→' : ' ';
223
+ const color = isCurrent ? chalk.blue : chalk.gray;
224
+ console.log(color(` ${prefix} ${i + 1}. ${formatStage(s)}`));
225
+ });
189
226
  } catch (error) {
190
227
  console.error(chalk.red(`\nāŒ Error: ${error.message}`));
191
228
  process.exit(1);
192
229
  }
193
230
  });
194
231
 
232
+ program
233
+ .command('modes')
234
+ .description('List available workflow modes')
235
+ .action(async () => {
236
+ const modeManager = engine.getModeManager();
237
+ const modes = await modeManager.compareModes();
238
+
239
+ console.log(chalk.bold('\nšŸ“Š Available Workflow Modes\n'));
240
+
241
+ modes.forEach(mode => {
242
+ console.log(chalk.bold.cyan(` ${mode.name.toUpperCase()}`));
243
+ console.log(chalk.white(` ${mode.description}`));
244
+ console.log(chalk.gray(` Stages: ${mode.stages.map(s => formatStage(s)).join(' → ')}`));
245
+ console.log(chalk.gray(` Coverage: ${mode.coverageThreshold}%`));
246
+ console.log(chalk.gray(` EARS: ${mode.earsRequired ? 'Required' : 'Optional'}`));
247
+ console.log(chalk.gray(` ADR: ${mode.adrRequired ? 'Required' : 'Optional'}`));
248
+ console.log();
249
+ });
250
+ });
251
+
252
+ /**
253
+ * Get mode description
254
+ */
255
+ function getModeDescription(mode) {
256
+ const descriptions = {
257
+ small: '1-2Ꙃ間',
258
+ medium: '1-2ę—„',
259
+ large: '1週間+',
260
+ };
261
+ return descriptions[mode] || mode;
262
+ }
263
+
195
264
  program.command('status').description('Show current workflow status').action(showStatus);
196
265
 
197
266
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musubi-sdd",
3
- "version": "5.8.2",
3
+ "version": "5.9.0",
4
4
  "description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -24,7 +24,9 @@
24
24
  "musubi-convert": "bin/musubi-convert.js",
25
25
  "musubi-browser": "bin/musubi-browser.js",
26
26
  "musubi-gui": "bin/musubi-gui.js",
27
- "musubi-orchestrate": "bin/musubi-orchestrate.js"
27
+ "musubi-orchestrate": "bin/musubi-orchestrate.js",
28
+ "musubi-release": "bin/musubi-release.js",
29
+ "musubi-config": "bin/musubi-config.js"
28
30
  },
29
31
  "scripts": {
30
32
  "test": "jest",
@@ -57,6 +59,8 @@
57
59
  },
58
60
  "dependencies": {
59
61
  "@octokit/rest": "^22.0.1",
62
+ "ajv": "^8.12.0",
63
+ "ajv-formats": "^2.1.1",
60
64
  "chalk": "^4.1.2",
61
65
  "chokidar": "^3.5.3",
62
66
  "commander": "^11.0.0",