specweave 0.22.4 → 0.22.6
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/CLAUDE.md +27 -0
- package/dist/src/cli/commands/init.d.ts +2 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +296 -179
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/migrate-to-profiles.d.ts.map +1 -1
- package/dist/src/cli/commands/migrate-to-profiles.js +4 -0
- package/dist/src/cli/commands/migrate-to-profiles.js.map +1 -1
- package/dist/src/cli/commands/next-command.d.ts +3 -0
- package/dist/src/cli/commands/next-command.d.ts.map +1 -1
- package/dist/src/cli/commands/next-command.js +7 -0
- package/dist/src/cli/commands/next-command.js.map +1 -1
- package/dist/src/cli/commands/validate-plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/validate-plugins.js +4 -0
- package/dist/src/cli/commands/validate-plugins.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +5238 -0
|
@@ -12,6 +12,7 @@ import { AgentsMdGenerator } from '../../adapters/agents-md-generator.js';
|
|
|
12
12
|
import { getDirname } from '../../utils/esm-helpers.js';
|
|
13
13
|
import { LanguageManager, isLanguageSupported, getSupportedLanguages } from '../../core/i18n/language-manager.js';
|
|
14
14
|
import { getLocaleManager } from '../../core/i18n/locale-manager.js';
|
|
15
|
+
import { consoleLogger } from '../../utils/logger.js';
|
|
15
16
|
const __dirname = getDirname(import.meta.url);
|
|
16
17
|
import { readEnvFile, parseEnvFile } from '../../utils/env-file.js';
|
|
17
18
|
/**
|
|
@@ -135,6 +136,18 @@ async function createMultiProjectFolders(targetDir) {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
export async function initCommand(projectName, options = {}) {
|
|
139
|
+
// Initialize logger (injectable for testing)
|
|
140
|
+
const logger = options.logger ?? consoleLogger;
|
|
141
|
+
// NOTE: This CLI command is 99% user-facing output (console.log/console.error).
|
|
142
|
+
// All console.* calls in this function are legitimate user-facing exceptions
|
|
143
|
+
// as defined in CONTRIBUTING.md (colored messages, confirmations, formatted output).
|
|
144
|
+
// Logger is available for any internal debug logs if needed in future.
|
|
145
|
+
// Detect CI/non-interactive environment (use throughout function)
|
|
146
|
+
const isCI = process.env.CI === 'true' ||
|
|
147
|
+
process.env.GITHUB_ACTIONS === 'true' ||
|
|
148
|
+
process.env.GITLAB_CI === 'true' ||
|
|
149
|
+
process.env.CIRCLECI === 'true' ||
|
|
150
|
+
!process.stdin.isTTY;
|
|
138
151
|
// Validate and normalize language option
|
|
139
152
|
const language = options.language?.toLowerCase() || 'en';
|
|
140
153
|
// Validate language if provided
|
|
@@ -182,22 +195,31 @@ export async function initCommand(projectName, options = {}) {
|
|
|
182
195
|
const dirName = path.basename(targetDir);
|
|
183
196
|
// Validate directory name is suitable for project name
|
|
184
197
|
if (!/^[a-z0-9-]+$/.test(dirName)) {
|
|
185
|
-
console.log(chalk.yellow(`\n${locale.t('cli', 'init.warnings.invalidDirName', { dirName })}`));
|
|
186
198
|
const suggestedName = dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
if (isCI) {
|
|
200
|
+
// CI mode: auto-sanitize directory name without prompting
|
|
201
|
+
console.log(chalk.yellow(`\n${locale.t('cli', 'init.warnings.invalidDirName', { dirName })}`));
|
|
202
|
+
console.log(chalk.gray(` → CI mode: Auto-sanitizing to "${suggestedName}"`));
|
|
203
|
+
finalProjectName = suggestedName;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Interactive mode: prompt user
|
|
207
|
+
console.log(chalk.yellow(`\n${locale.t('cli', 'init.warnings.invalidDirName', { dirName })}`));
|
|
208
|
+
const { name } = await inquirer.prompt([
|
|
209
|
+
{
|
|
210
|
+
type: 'input',
|
|
211
|
+
name: 'name',
|
|
212
|
+
message: 'Project name (for templates):',
|
|
213
|
+
default: suggestedName,
|
|
214
|
+
validate: (input) => {
|
|
215
|
+
if (/^[a-z0-9-]+$/.test(input))
|
|
216
|
+
return true;
|
|
217
|
+
return 'Project name must be lowercase letters, numbers, and hyphens only';
|
|
218
|
+
},
|
|
197
219
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
]);
|
|
221
|
+
finalProjectName = name;
|
|
222
|
+
}
|
|
201
223
|
}
|
|
202
224
|
else {
|
|
203
225
|
finalProjectName = dirName;
|
|
@@ -206,18 +228,25 @@ export async function initCommand(projectName, options = {}) {
|
|
|
206
228
|
const allFiles = fs.readdirSync(targetDir);
|
|
207
229
|
const existingFiles = allFiles.filter(f => !f.startsWith('.')); // Ignore hidden files
|
|
208
230
|
if (existingFiles.length > 0 && !options.force) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
if (isCI) {
|
|
232
|
+
// CI mode: allow initialization in non-empty directory without prompting
|
|
233
|
+
console.log(chalk.yellow(`\n${locale.t('cli', 'init.warnings.directoryNotEmpty', { count: existingFiles.length, plural: existingFiles.length === 1 ? '' : 's' })}`));
|
|
234
|
+
console.log(chalk.gray(` → CI mode: Proceeding with initialization`));
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
console.log(chalk.yellow(`\n${locale.t('cli', 'init.warnings.directoryNotEmpty', { count: existingFiles.length, plural: existingFiles.length === 1 ? '' : 's' })}`));
|
|
238
|
+
const { confirm } = await inquirer.prompt([
|
|
239
|
+
{
|
|
240
|
+
type: 'confirm',
|
|
241
|
+
name: 'confirm',
|
|
242
|
+
message: 'Initialize SpecWeave in current directory?',
|
|
243
|
+
default: false,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
if (!confirm) {
|
|
247
|
+
console.log(chalk.yellow(locale.t('cli', 'init.errors.cancelled')));
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
252
|
// Check if .specweave already exists - SMART RE-INITIALIZATION
|
|
@@ -233,23 +262,35 @@ export async function initCommand(projectName, options = {}) {
|
|
|
233
262
|
console.log(chalk.red(' • All documentation (.specweave/docs/)'));
|
|
234
263
|
console.log(chalk.red(' • All configuration and history'));
|
|
235
264
|
console.log(chalk.yellow('\n 💡 TIP: Use "specweave init ." (no --force) for safe updates\n'));
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
if (isCI) {
|
|
266
|
+
// CI mode: proceed with force deletion without prompting (test environment)
|
|
267
|
+
console.log(chalk.gray(' → CI mode: Proceeding with force deletion'));
|
|
268
|
+
action = 'fresh';
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Interactive mode: ALWAYS require confirmation, even in force mode (safety critical!)
|
|
272
|
+
const { confirmDeletion } = await inquirer.prompt([
|
|
273
|
+
{
|
|
274
|
+
type: 'confirm',
|
|
275
|
+
name: 'confirmDeletion',
|
|
276
|
+
message: chalk.red('⚠️ Type "y" to PERMANENTLY DELETE all .specweave/ data:'),
|
|
277
|
+
default: false,
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
if (!confirmDeletion) {
|
|
281
|
+
console.log(chalk.green('\n✅ Deletion cancelled. No data lost.'));
|
|
282
|
+
console.log(chalk.gray(' → Run "specweave init ." (without --force) for safe updates'));
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
action = 'fresh';
|
|
249
286
|
}
|
|
250
|
-
action = 'fresh';
|
|
251
287
|
console.log(chalk.yellow('\n 🔄 Force mode: Proceeding with fresh start...'));
|
|
252
288
|
}
|
|
289
|
+
else if (isCI) {
|
|
290
|
+
// CI mode: auto-continue with existing project
|
|
291
|
+
console.log(chalk.gray(' → CI mode: Continuing with existing project'));
|
|
292
|
+
action = 'continue';
|
|
293
|
+
}
|
|
253
294
|
else {
|
|
254
295
|
// Interactive mode: Ask user what to do
|
|
255
296
|
const result = await inquirer.prompt([
|
|
@@ -285,6 +326,12 @@ export async function initCommand(projectName, options = {}) {
|
|
|
285
326
|
}
|
|
286
327
|
if (action === 'fresh') {
|
|
287
328
|
if (!options.force) {
|
|
329
|
+
if (isCI) {
|
|
330
|
+
// CI mode: NEVER allow fresh start without explicit --force flag (safety critical!)
|
|
331
|
+
console.log(chalk.red('\n⛔ ERROR: Cannot start fresh in CI mode without --force flag'));
|
|
332
|
+
console.log(chalk.gray(' → Use "specweave init . --force" if you really want to delete all data'));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
288
335
|
// Interactive mode: Ask for confirmation (force mode already confirmed above)
|
|
289
336
|
console.log(chalk.yellow('\n⚠️ WARNING: This will DELETE all increments, docs, and configuration!'));
|
|
290
337
|
const { confirmFresh } = await inquirer.prompt([
|
|
@@ -576,12 +623,7 @@ export async function initCommand(projectName, options = {}) {
|
|
|
576
623
|
}
|
|
577
624
|
}
|
|
578
625
|
console.log('');
|
|
579
|
-
//
|
|
580
|
-
const isCI = process.env.CI === 'true' ||
|
|
581
|
-
process.env.GITHUB_ACTIONS === 'true' ||
|
|
582
|
-
process.env.GITLAB_CI === 'true' ||
|
|
583
|
-
process.env.CIRCLECI === 'true' ||
|
|
584
|
-
!process.stdin.isTTY;
|
|
626
|
+
// Use function-level isCI (already defined at function start)
|
|
585
627
|
let confirmTool = true; // Default to yes
|
|
586
628
|
if (isCI) {
|
|
587
629
|
// In CI, automatically use detected tool without prompting
|
|
@@ -798,102 +840,8 @@ export async function initCommand(projectName, options = {}) {
|
|
|
798
840
|
});
|
|
799
841
|
}
|
|
800
842
|
}
|
|
801
|
-
// 12.
|
|
802
|
-
|
|
803
|
-
let coverageTarget = 80;
|
|
804
|
-
// Only prompt if interactive (not CI)
|
|
805
|
-
const isCI = process.env.CI === 'true' ||
|
|
806
|
-
process.env.GITHUB_ACTIONS === 'true' ||
|
|
807
|
-
process.env.GITLAB_CI === 'true' ||
|
|
808
|
-
process.env.CIRCLECI === 'true' ||
|
|
809
|
-
!process.stdin.isTTY;
|
|
810
|
-
if (!isCI && !continueExisting) {
|
|
811
|
-
console.log('');
|
|
812
|
-
console.log(chalk.cyan.bold('🧪 Testing Configuration'));
|
|
813
|
-
console.log(chalk.gray(' Configure your default testing approach and coverage targets'));
|
|
814
|
-
console.log('');
|
|
815
|
-
const { selectedTestMode } = await inquirer.prompt([
|
|
816
|
-
{
|
|
817
|
-
type: 'list',
|
|
818
|
-
name: 'selectedTestMode',
|
|
819
|
-
message: 'Select your testing approach:',
|
|
820
|
-
choices: [
|
|
821
|
-
{
|
|
822
|
-
name: 'TDD (Test-Driven Development) - Write tests first',
|
|
823
|
-
value: 'TDD',
|
|
824
|
-
short: 'TDD'
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
name: 'Test-After - Implement first, then write tests',
|
|
828
|
-
value: 'test-after',
|
|
829
|
-
short: 'Test-After'
|
|
830
|
-
},
|
|
831
|
-
{
|
|
832
|
-
name: 'Manual Testing - No automated tests',
|
|
833
|
-
value: 'manual',
|
|
834
|
-
short: 'Manual'
|
|
835
|
-
}
|
|
836
|
-
],
|
|
837
|
-
default: 'TDD'
|
|
838
|
-
}
|
|
839
|
-
]);
|
|
840
|
-
testMode = selectedTestMode;
|
|
841
|
-
const { selectedCoverageLevel } = await inquirer.prompt([
|
|
842
|
-
{
|
|
843
|
-
type: 'list',
|
|
844
|
-
name: 'selectedCoverageLevel',
|
|
845
|
-
message: 'Select your coverage target level:',
|
|
846
|
-
choices: [
|
|
847
|
-
{
|
|
848
|
-
name: '70% - Acceptable (core paths covered)',
|
|
849
|
-
value: 70,
|
|
850
|
-
short: '70%'
|
|
851
|
-
},
|
|
852
|
-
{
|
|
853
|
-
name: '80% - Good (recommended - most paths covered)',
|
|
854
|
-
value: 80,
|
|
855
|
-
short: '80%'
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
name: '90% - Excellent (comprehensive coverage)',
|
|
859
|
-
value: 90,
|
|
860
|
-
short: '90%'
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
name: 'Custom (enter your own value)',
|
|
864
|
-
value: 'custom',
|
|
865
|
-
short: 'Custom'
|
|
866
|
-
}
|
|
867
|
-
],
|
|
868
|
-
default: 80
|
|
869
|
-
}
|
|
870
|
-
]);
|
|
871
|
-
if (selectedCoverageLevel === 'custom') {
|
|
872
|
-
const { customCoverage } = await inquirer.prompt([
|
|
873
|
-
{
|
|
874
|
-
type: 'number',
|
|
875
|
-
name: 'customCoverage',
|
|
876
|
-
message: 'Enter custom coverage target (70-95):',
|
|
877
|
-
default: 80,
|
|
878
|
-
validate: (input) => {
|
|
879
|
-
if (input >= 70 && input <= 95)
|
|
880
|
-
return true;
|
|
881
|
-
return 'Coverage target must be between 70% and 95%';
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
]);
|
|
885
|
-
coverageTarget = customCoverage;
|
|
886
|
-
}
|
|
887
|
-
else {
|
|
888
|
-
coverageTarget = selectedCoverageLevel;
|
|
889
|
-
}
|
|
890
|
-
console.log('');
|
|
891
|
-
console.log(chalk.green(` ✔ Testing: ${testMode}`));
|
|
892
|
-
console.log(chalk.green(` ✔ Coverage Target: ${coverageTarget}%`));
|
|
893
|
-
console.log('');
|
|
894
|
-
}
|
|
895
|
-
// 13. Create config.json with language and testing settings
|
|
896
|
-
createConfigFile(targetDir, finalProjectName, toolName, language, false, testMode, coverageTarget);
|
|
843
|
+
// 12. Create config.json with basic settings (testing params added later)
|
|
844
|
+
createConfigFile(targetDir, finalProjectName, toolName, language, false);
|
|
897
845
|
// 14. AUTO-INSTALL ALL PLUGINS via Claude CLI (Breaking Change: No selective loading)
|
|
898
846
|
// NOTE: We do NOT create .claude/settings.json - marketplace registration via CLI is GLOBAL
|
|
899
847
|
// and persists across all projects. settings.json would be redundant.
|
|
@@ -960,7 +908,8 @@ export async function initCommand(projectName, options = {}) {
|
|
|
960
908
|
else {
|
|
961
909
|
// Claude CLI available → install ALL plugins from marketplace
|
|
962
910
|
try {
|
|
963
|
-
// Step 1:
|
|
911
|
+
// Step 1: Remove existing marketplace to force update (REQUIRED for updates!)
|
|
912
|
+
// This ensures users get the latest plugins when new ones are added
|
|
964
913
|
spinner.start('Refreshing SpecWeave marketplace...');
|
|
965
914
|
const listResult = execFileNoThrowSync('claude', [
|
|
966
915
|
'plugin',
|
|
@@ -970,16 +919,15 @@ export async function initCommand(projectName, options = {}) {
|
|
|
970
919
|
const marketplaceExists = listResult.success &&
|
|
971
920
|
(listResult.stdout || '').toLowerCase().includes('specweave');
|
|
972
921
|
if (marketplaceExists) {
|
|
973
|
-
// Always remove existing marketplace to ensure fresh install
|
|
974
922
|
execFileNoThrowSync('claude', [
|
|
975
923
|
'plugin',
|
|
976
924
|
'marketplace',
|
|
977
925
|
'remove',
|
|
978
926
|
'specweave'
|
|
979
927
|
]);
|
|
980
|
-
console.log(chalk.blue(' 🔄 Removed existing marketplace'));
|
|
928
|
+
console.log(chalk.blue(' 🔄 Removed existing marketplace for update'));
|
|
981
929
|
}
|
|
982
|
-
// Add marketplace from GitHub (always fresh)
|
|
930
|
+
// Step 2: Add marketplace from GitHub (always fresh)
|
|
983
931
|
const addResult = execFileNoThrowSync('claude', [
|
|
984
932
|
'plugin',
|
|
985
933
|
'marketplace',
|
|
@@ -990,7 +938,13 @@ export async function initCommand(projectName, options = {}) {
|
|
|
990
938
|
throw new Error('Failed to add marketplace from GitHub');
|
|
991
939
|
}
|
|
992
940
|
console.log(chalk.green(' ✔ Marketplace registered from GitHub'));
|
|
993
|
-
|
|
941
|
+
// CRITICAL: Wait for marketplace cache to fully initialize after remove+add
|
|
942
|
+
// The remove operation invalidates cache, add fetches from GitHub
|
|
943
|
+
// We need to wait for: fetch + parse + validate 25 plugins
|
|
944
|
+
// 2 seconds was too short, increasing to 5 seconds for reliability
|
|
945
|
+
spinner.text = 'Initializing marketplace cache (this takes a few seconds)...';
|
|
946
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay
|
|
947
|
+
spinner.succeed('SpecWeave marketplace ready');
|
|
994
948
|
// Step 2: Load marketplace.json to get ALL available plugins
|
|
995
949
|
spinner.start('Loading available plugins...');
|
|
996
950
|
const marketplaceJsonPath = findSourceDir('.claude-plugin/marketplace.json');
|
|
@@ -1004,19 +958,35 @@ export async function initCommand(projectName, options = {}) {
|
|
|
1004
958
|
}
|
|
1005
959
|
console.log(chalk.blue(` 📦 Found ${allPlugins.length} plugins to install`));
|
|
1006
960
|
spinner.succeed(`Found ${allPlugins.length} plugins`);
|
|
1007
|
-
// Step 3: Install ALL plugins (
|
|
961
|
+
// Step 3: Install ALL plugins with retry logic (handles remaining race conditions)
|
|
1008
962
|
let successCount = 0;
|
|
1009
963
|
let failCount = 0;
|
|
1010
964
|
const failedPlugins = [];
|
|
1011
965
|
for (const plugin of allPlugins) {
|
|
1012
966
|
const pluginName = plugin.name;
|
|
1013
967
|
spinner.start(`Installing ${pluginName}...`);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
968
|
+
// Retry up to 3 times with exponential backoff
|
|
969
|
+
let installed = false;
|
|
970
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
971
|
+
const installResult = execFileNoThrowSync('claude', [
|
|
972
|
+
'plugin',
|
|
973
|
+
'install',
|
|
974
|
+
pluginName
|
|
975
|
+
]);
|
|
976
|
+
if (installResult.success) {
|
|
977
|
+
installed = true;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
// If "not found" error and not last attempt, wait and retry
|
|
981
|
+
if (installResult.stderr?.includes('not found') && attempt < 3) {
|
|
982
|
+
spinner.text = `Installing ${pluginName}... (retry ${attempt}/3)`;
|
|
983
|
+
await new Promise(resolve => setTimeout(resolve, 500 * attempt)); // 500ms, 1s, 1.5s
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
// Other errors or final attempt - stop retrying
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
if (installed) {
|
|
1020
990
|
successCount++;
|
|
1021
991
|
spinner.succeed(`${pluginName} installed`);
|
|
1022
992
|
}
|
|
@@ -1101,23 +1071,29 @@ export async function initCommand(projectName, options = {}) {
|
|
|
1101
1071
|
console.log(chalk.blue('\n🔍 Existing Issue Tracker Configuration Detected'));
|
|
1102
1072
|
console.log(chalk.gray(` Current: ${existingTracker.charAt(0).toUpperCase() + existingTracker.slice(1)}`));
|
|
1103
1073
|
console.log('');
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
message: 'Do you want to reconfigure your issue tracker?',
|
|
1108
|
-
default: false
|
|
1109
|
-
}]);
|
|
1110
|
-
if (!reconfigure) {
|
|
1111
|
-
console.log(chalk.gray(' ✓ Keeping existing configuration\n'));
|
|
1074
|
+
if (isCI) {
|
|
1075
|
+
// CI mode: keep existing configuration without prompting
|
|
1076
|
+
console.log(chalk.gray(' → CI mode: Keeping existing configuration\n'));
|
|
1112
1077
|
}
|
|
1113
1078
|
else {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1079
|
+
const { reconfigure } = await inquirer.prompt([{
|
|
1080
|
+
type: 'confirm',
|
|
1081
|
+
name: 'reconfigure',
|
|
1082
|
+
message: 'Do you want to reconfigure your issue tracker?',
|
|
1083
|
+
default: false
|
|
1084
|
+
}]);
|
|
1085
|
+
if (!reconfigure) {
|
|
1086
|
+
console.log(chalk.gray(' ✓ Keeping existing configuration\n'));
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
// User wants to reconfigure - run setup
|
|
1090
|
+
await setupIssueTracker({
|
|
1091
|
+
projectPath: targetDir,
|
|
1092
|
+
language: language,
|
|
1093
|
+
maxRetries: 3,
|
|
1094
|
+
isFrameworkRepo
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1121
1097
|
}
|
|
1122
1098
|
}
|
|
1123
1099
|
else {
|
|
@@ -1154,6 +1130,122 @@ export async function initCommand(projectName, options = {}) {
|
|
|
1154
1130
|
}
|
|
1155
1131
|
}
|
|
1156
1132
|
}
|
|
1133
|
+
// 10.7 Testing Configuration (MOVED TO END - Better UX)
|
|
1134
|
+
// Prompt for testing approach and coverage targets after all setup is complete
|
|
1135
|
+
// This keeps the main flow fast and asks for preferences at the end
|
|
1136
|
+
let testMode = 'TDD';
|
|
1137
|
+
let coverageTarget = 80;
|
|
1138
|
+
// Only prompt if interactive (use function-level isCI)
|
|
1139
|
+
if (!isCI && !continueExisting) {
|
|
1140
|
+
console.log('');
|
|
1141
|
+
console.log(chalk.cyan.bold('🧪 Testing Configuration'));
|
|
1142
|
+
console.log(chalk.gray(' Configure your default testing approach and coverage targets'));
|
|
1143
|
+
console.log('');
|
|
1144
|
+
// Add guidance on which testing approach to choose
|
|
1145
|
+
console.log(chalk.white('💡 Which testing approach should you choose?'));
|
|
1146
|
+
console.log('');
|
|
1147
|
+
console.log(chalk.green(' ✓ TDD (Test-Driven Development)'));
|
|
1148
|
+
console.log(chalk.gray(' Best for: Complex business logic, critical features, refactoring'));
|
|
1149
|
+
console.log(chalk.gray(' Benefits: Better design, fewer bugs, confidence in changes'));
|
|
1150
|
+
console.log(chalk.gray(' Tradeoff: Slower initial development, requires discipline'));
|
|
1151
|
+
console.log('');
|
|
1152
|
+
console.log(chalk.blue(' ✓ Test-After'));
|
|
1153
|
+
console.log(chalk.gray(' Best for: Most projects, rapid prototyping, exploratory work'));
|
|
1154
|
+
console.log(chalk.gray(' Benefits: Fast iteration, flexible design, good coverage'));
|
|
1155
|
+
console.log(chalk.gray(' Tradeoff: May miss edge cases, harder to test after design'));
|
|
1156
|
+
console.log('');
|
|
1157
|
+
console.log(chalk.yellow(' ✓ Manual Testing'));
|
|
1158
|
+
console.log(chalk.gray(' Best for: Quick prototypes, proof-of-concepts, learning'));
|
|
1159
|
+
console.log(chalk.gray(' Benefits: Fastest development, no test maintenance'));
|
|
1160
|
+
console.log(chalk.gray(' Tradeoff: No safety net, regressions, hard to refactor'));
|
|
1161
|
+
console.log('');
|
|
1162
|
+
const { selectedTestMode } = await inquirer.prompt([
|
|
1163
|
+
{
|
|
1164
|
+
type: 'list',
|
|
1165
|
+
name: 'selectedTestMode',
|
|
1166
|
+
message: 'Select your testing approach:',
|
|
1167
|
+
choices: [
|
|
1168
|
+
{
|
|
1169
|
+
name: '🔴 TDD - Write tests first (recommended for production apps)',
|
|
1170
|
+
value: 'TDD',
|
|
1171
|
+
short: 'TDD'
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: '🔵 Test-After - Implement first, test later (balanced approach)',
|
|
1175
|
+
value: 'test-after',
|
|
1176
|
+
short: 'Test-After'
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
name: '🟡 Manual - No automated tests (prototypes only)',
|
|
1180
|
+
value: 'manual',
|
|
1181
|
+
short: 'Manual'
|
|
1182
|
+
}
|
|
1183
|
+
],
|
|
1184
|
+
default: 'TDD'
|
|
1185
|
+
}
|
|
1186
|
+
]);
|
|
1187
|
+
testMode = selectedTestMode;
|
|
1188
|
+
// Only ask for coverage if not manual testing
|
|
1189
|
+
if (testMode !== 'manual') {
|
|
1190
|
+
const { selectedCoverageLevel } = await inquirer.prompt([
|
|
1191
|
+
{
|
|
1192
|
+
type: 'list',
|
|
1193
|
+
name: 'selectedCoverageLevel',
|
|
1194
|
+
message: 'Select your coverage target level:',
|
|
1195
|
+
choices: [
|
|
1196
|
+
{
|
|
1197
|
+
name: '70% - Acceptable (core paths covered)',
|
|
1198
|
+
value: 70,
|
|
1199
|
+
short: '70%'
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: '80% - Good (recommended - most paths covered)',
|
|
1203
|
+
value: 80,
|
|
1204
|
+
short: '80%'
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
name: '90% - Excellent (comprehensive coverage)',
|
|
1208
|
+
value: 90,
|
|
1209
|
+
short: '90%'
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
name: 'Custom (enter your own value)',
|
|
1213
|
+
value: 'custom',
|
|
1214
|
+
short: 'Custom'
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
default: 80
|
|
1218
|
+
}
|
|
1219
|
+
]);
|
|
1220
|
+
if (selectedCoverageLevel === 'custom') {
|
|
1221
|
+
const { customCoverage } = await inquirer.prompt([
|
|
1222
|
+
{
|
|
1223
|
+
type: 'number',
|
|
1224
|
+
name: 'customCoverage',
|
|
1225
|
+
message: 'Enter custom coverage target (70-95):',
|
|
1226
|
+
default: 80,
|
|
1227
|
+
validate: (input) => {
|
|
1228
|
+
if (input >= 70 && input <= 95)
|
|
1229
|
+
return true;
|
|
1230
|
+
return 'Coverage target must be between 70% and 95%';
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
]);
|
|
1234
|
+
coverageTarget = customCoverage;
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
coverageTarget = selectedCoverageLevel;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
console.log('');
|
|
1241
|
+
console.log(chalk.green(` ✔ Testing: ${testMode}`));
|
|
1242
|
+
if (testMode !== 'manual') {
|
|
1243
|
+
console.log(chalk.green(` ✔ Coverage Target: ${coverageTarget}%`));
|
|
1244
|
+
}
|
|
1245
|
+
console.log('');
|
|
1246
|
+
// Update config.json with testing configuration
|
|
1247
|
+
updateConfigWithTesting(targetDir, testMode, coverageTarget);
|
|
1248
|
+
}
|
|
1157
1249
|
showNextSteps(finalProjectName, toolName, language, usedDotNotation, toolName === 'claude' ? autoInstallSucceeded : undefined);
|
|
1158
1250
|
}
|
|
1159
1251
|
catch (error) {
|
|
@@ -1359,8 +1451,9 @@ function findSourceDir(relativePath) {
|
|
|
1359
1451
|
}
|
|
1360
1452
|
/**
|
|
1361
1453
|
* Create .specweave/config.json with project settings
|
|
1454
|
+
* Testing configuration is optional and can be added later via updateConfigWithTesting()
|
|
1362
1455
|
*/
|
|
1363
|
-
function createConfigFile(targetDir, projectName, adapter, language, enableDocsPreview = true, testMode
|
|
1456
|
+
function createConfigFile(targetDir, projectName, adapter, language, enableDocsPreview = true, testMode, coverageTarget) {
|
|
1364
1457
|
const configPath = path.join(targetDir, '.specweave', 'config.json');
|
|
1365
1458
|
const config = {
|
|
1366
1459
|
project: {
|
|
@@ -1370,16 +1463,18 @@ function createConfigFile(targetDir, projectName, adapter, language, enableDocsP
|
|
|
1370
1463
|
adapters: {
|
|
1371
1464
|
default: adapter,
|
|
1372
1465
|
},
|
|
1373
|
-
// Testing configuration (NEW - v0.18.0+)
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1466
|
+
// Testing configuration (NEW - v0.18.0+) - only include if provided
|
|
1467
|
+
...(testMode && coverageTarget && {
|
|
1468
|
+
testing: {
|
|
1469
|
+
defaultTestMode: testMode,
|
|
1470
|
+
defaultCoverageTarget: coverageTarget,
|
|
1471
|
+
coverageTargets: {
|
|
1472
|
+
unit: Math.min(coverageTarget + 5, 95), // Unit tests slightly higher
|
|
1473
|
+
integration: coverageTarget, // Integration at default
|
|
1474
|
+
e2e: Math.min(coverageTarget + 10, 100) // E2E tests highest (critical paths)
|
|
1475
|
+
}
|
|
1381
1476
|
}
|
|
1382
|
-
},
|
|
1477
|
+
}),
|
|
1383
1478
|
// Documentation preview settings (for Claude Code only)
|
|
1384
1479
|
...(adapter === 'claude' && {
|
|
1385
1480
|
documentation: {
|
|
@@ -1408,6 +1503,28 @@ function createConfigFile(targetDir, projectName, adapter, language, enableDocsP
|
|
|
1408
1503
|
};
|
|
1409
1504
|
fs.writeJsonSync(configPath, config, { spaces: 2 });
|
|
1410
1505
|
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Update config.json with testing configuration
|
|
1508
|
+
* Called after user completes testing setup prompts
|
|
1509
|
+
*/
|
|
1510
|
+
function updateConfigWithTesting(targetDir, testMode, coverageTarget) {
|
|
1511
|
+
const configPath = path.join(targetDir, '.specweave', 'config.json');
|
|
1512
|
+
if (!fs.existsSync(configPath)) {
|
|
1513
|
+
console.error(chalk.red('⚠️ config.json not found, cannot update testing configuration'));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const config = fs.readJsonSync(configPath);
|
|
1517
|
+
config.testing = {
|
|
1518
|
+
defaultTestMode: testMode,
|
|
1519
|
+
defaultCoverageTarget: coverageTarget,
|
|
1520
|
+
coverageTargets: {
|
|
1521
|
+
unit: Math.min(coverageTarget + 5, 95),
|
|
1522
|
+
integration: coverageTarget,
|
|
1523
|
+
e2e: Math.min(coverageTarget + 10, 100)
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
fs.writeJsonSync(configPath, config, { spaces: 2 });
|
|
1527
|
+
}
|
|
1411
1528
|
/**
|
|
1412
1529
|
* REMOVED: setupClaudePluginAutoRegistration()
|
|
1413
1530
|
*
|