specweave 1.0.418 → 1.0.419

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.
Files changed (53) hide show
  1. package/dist/src/cli/commands/init.d.ts +4 -11
  2. package/dist/src/cli/commands/init.d.ts.map +1 -1
  3. package/dist/src/cli/commands/init.js +126 -732
  4. package/dist/src/cli/commands/init.js.map +1 -1
  5. package/dist/src/cli/commands/resolve-structure.d.ts +4 -2
  6. package/dist/src/cli/commands/resolve-structure.d.ts.map +1 -1
  7. package/dist/src/cli/commands/resolve-structure.js +4 -2
  8. package/dist/src/cli/commands/resolve-structure.js.map +1 -1
  9. package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts.map +1 -1
  10. package/dist/src/cli/helpers/init/ado-repo-cloning.js +14 -12
  11. package/dist/src/cli/helpers/init/ado-repo-cloning.js.map +1 -1
  12. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.d.ts.map +1 -1
  13. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js +14 -12
  14. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js.map +1 -1
  15. package/dist/src/cli/helpers/init/directory-structure.d.ts +2 -2
  16. package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
  17. package/dist/src/cli/helpers/init/directory-structure.js +5 -23
  18. package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
  19. package/dist/src/cli/helpers/init/github-repo-cloning.d.ts.map +1 -1
  20. package/dist/src/cli/helpers/init/github-repo-cloning.js +14 -12
  21. package/dist/src/cli/helpers/init/github-repo-cloning.js.map +1 -1
  22. package/dist/src/cli/helpers/init/index.d.ts +5 -7
  23. package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
  24. package/dist/src/cli/helpers/init/index.js +6 -17
  25. package/dist/src/cli/helpers/init/index.js.map +1 -1
  26. package/dist/src/cli/helpers/init/next-steps.d.ts +6 -6
  27. package/dist/src/cli/helpers/init/next-steps.d.ts.map +1 -1
  28. package/dist/src/cli/helpers/init/next-steps.js +49 -52
  29. package/dist/src/cli/helpers/init/next-steps.js.map +1 -1
  30. package/dist/src/cli/helpers/init/smart-defaults.js +1 -1
  31. package/dist/src/cli/helpers/init/smart-defaults.js.map +1 -1
  32. package/dist/src/cli/helpers/init/summary-banner.d.ts +2 -21
  33. package/dist/src/cli/helpers/init/summary-banner.d.ts.map +1 -1
  34. package/dist/src/cli/helpers/init/summary-banner.js +3 -49
  35. package/dist/src/cli/helpers/init/summary-banner.js.map +1 -1
  36. package/dist/src/config/types.d.ts +8 -8
  37. package/dist/src/core/background/job-launcher.d.ts +2 -0
  38. package/dist/src/core/background/job-launcher.d.ts.map +1 -1
  39. package/dist/src/core/background/job-launcher.js +7 -8
  40. package/dist/src/core/background/job-launcher.js.map +1 -1
  41. package/dist/src/init/architecture/types.d.ts +2 -2
  42. package/dist/src/init/compliance/types.d.ts +2 -2
  43. package/package.json +1 -1
  44. package/plugins/specweave/hooks/v2/guards/increment-existence-guard.sh +11 -80
  45. package/plugins/specweave/skills/team-build/SKILL.md +39 -120
  46. package/plugins/specweave/skills/team-lead/SKILL.md +232 -494
  47. package/src/templates/config.json.template +2 -77
  48. package/plugins/specweave/skills/team-lead/agents/brainstorm-advocate.md +0 -65
  49. package/plugins/specweave/skills/team-lead/agents/brainstorm-critic.md +0 -75
  50. package/plugins/specweave/skills/team-lead/agents/brainstorm-pragmatist.md +0 -83
  51. package/plugins/specweave/skills/team-lead/agents/reviewer-logic.md +0 -63
  52. package/plugins/specweave/skills/team-lead/agents/reviewer-performance.md +0 -63
  53. package/plugins/specweave/skills/team-lead/agents/reviewer-security.md +0 -62
@@ -1,43 +1,30 @@
1
1
  /**
2
2
  * SpecWeave Init Command
3
3
  *
4
- * Initializes a new SpecWeave project with:
5
- * - Directory structure (.specweave/)
6
- * - Configuration files
7
- * - Plugin installation (Claude Code)
8
- * - Issue tracker setup
9
- * - Initial increment
10
- *
11
- * Refactored: Logic extracted to src/cli/helpers/init/
4
+ * Simplified (v1.0.415): Creates .specweave/ structure + config.json + instruction files.
5
+ * External tool setup moved to `specweave sync-setup`.
6
+ * Multi-repo setup moved to `specweave migrate-to-umbrella`.
12
7
  */
13
8
  import * as fs from '../../utils/fs-native.js';
14
9
  import * as path from 'path';
15
10
  import * as os from 'os';
16
11
  import chalk from 'chalk';
17
12
  import ora from 'ora';
18
- import { input, confirm, select } from '@inquirer/prompts';
13
+ import { input, confirm } from '@inquirer/prompts';
19
14
  import { execFileNoThrowSync } from '../../utils/execFileNoThrow.js';
20
15
  import { AdapterLoader } from '../../adapters/adapter-loader.js';
21
16
  import { getDirname } from '../../utils/esm-helpers.js';
22
17
  import { isLanguageSupported, getSupportedLanguages } from '../../core/i18n/language-manager.js';
23
18
  import { getLocaleManager } from '../../core/i18n/locale-manager.js';
24
- import { readEnvFile, parseEnvFile } from '../../utils/env-file.js';
25
- // Import helpers
26
- import { findSourceDir, findPackageRoot, detectNestedSpecweave, detectUmbrellaParent, detectSuspiciousPath, detectGitHubRemote, promptSmartReinit, installAllPlugins, setupRepositoryHosting, promptLanguageSelection, getDefaultLanguageSelection, createDirectoryStructure, copyTemplates, createConfigFile, showNextSteps, installGitHooks, } from '../helpers/init/index.js';
27
- import { triggerAdoRepoCloning } from '../helpers/init/ado-repo-cloning.js';
28
- import { triggerGitHubRepoCloning } from '../helpers/init/github-repo-cloning.js';
29
- import { triggerBitbucketRepoCloning } from '../helpers/init/bitbucket-repo-cloning.js';
30
- import { createProjectFolders } from '../helpers/init/multi-project-folders.js';
19
+ import { findSourceDir, findPackageRoot, detectNestedSpecweave, detectUmbrellaParent, detectSuspiciousPath, detectProvider, promptSmartReinit, installAllPlugins, promptLanguageSelection, getDefaultLanguageSelection, createDirectoryStructure, copyTemplates, createConfigFile, showNextSteps, installGitHooks, } from '../helpers/init/index.js';
31
20
  import { setupLspEnvVar } from '../helpers/init/shell-config.js';
32
- import { getPluginScope, getScopeArgs } from '../../core/types/plugin-scope.js';
33
21
  import { applySmartDefaults } from '../helpers/init/smart-defaults.js';
34
- import { isGreenfield as isGreenfieldCheck } from '../helpers/init/greenfield-detection.js';
35
22
  import { displaySummaryBanner } from '../helpers/init/summary-banner.js';
36
23
  const __dirname = getDirname(import.meta.url);
24
+ const PROJECT_NAME_PATTERN = /^[a-z0-9-]+$/;
25
+ const PROJECT_NAME_VALIDATION_MSG = 'Must be lowercase letters, numbers, and hyphens only';
37
26
  /**
38
27
  * Unified CI/non-interactive detection.
39
- * Single source of truth for determining if wizard should skip prompts.
40
- * Covers all major CI platforms and non-TTY environments.
41
28
  */
42
29
  export function isNonInteractive(options) {
43
30
  return !!(options.quick ||
@@ -49,180 +36,21 @@ export function isNonInteractive(options) {
49
36
  !process.stdin.isTTY);
50
37
  }
51
38
  /**
52
- * Detect if we're in the SpecWeave framework repository itself
53
- */
54
- async function isSpecWeaveFrameworkRepo(targetDir) {
55
- try {
56
- const packageJsonPath = path.join(targetDir, 'package.json');
57
- if (!fs.existsSync(packageJsonPath)) {
58
- return false;
59
- }
60
- const packageJson = await fs.readJson(packageJsonPath);
61
- return packageJson.name === 'specweave';
62
- }
63
- catch {
64
- return false;
65
- }
66
- }
67
- /**
68
- * Create Multi-Project Folders based on Issue Tracker Configuration
69
- */
70
- async function createMultiProjectFolders(targetDir) {
71
- const envPath = path.join(targetDir, '.env');
72
- const configPath = path.join(targetDir, '.specweave', 'config.json');
73
- if (!fs.existsSync(envPath)) {
74
- return;
75
- }
76
- const envContent = readEnvFile(envPath);
77
- const envVars = parseEnvFile(envContent);
78
- const jiraProjects = envVars.JIRA_PROJECTS?.split(',').map((p) => p.trim()).filter(Boolean);
79
- const jiraStrategy = envVars.JIRA_STRATEGY;
80
- if (!jiraProjects?.length) {
81
- return;
82
- }
83
- let config = {};
84
- if (fs.existsSync(configPath)) {
85
- config = await fs.readJson(configPath);
86
- }
87
- if (!config.sync) {
88
- config.sync = {
89
- enabled: true,
90
- profiles: {},
91
- defaultProfile: undefined,
92
- settings: {
93
- autoCreateIssue: true,
94
- syncDirection: 'bidirectional'
95
- }
96
- };
97
- }
98
- if (jiraProjects?.length && jiraStrategy === 'project-per-team') {
99
- const profileId = 'jira-default';
100
- const syncConfig = config.sync;
101
- const profiles = syncConfig.profiles;
102
- if (!profiles[profileId]) {
103
- const jiraProfile = {
104
- provider: 'jira',
105
- displayName: 'Jira Default',
106
- config: {
107
- domain: envVars.JIRA_DOMAIN || '',
108
- projects: jiraProjects
109
- },
110
- timeRange: { default: '1M', max: '6M' },
111
- rateLimits: { maxItemsPerSync: 500, warnThreshold: 100 }
112
- };
113
- profiles[profileId] = jiraProfile;
114
- syncConfig.defaultProfile = profileId;
115
- await fs.writeJson(configPath, config, { spaces: 2 });
116
- console.log(chalk.blue('\nšŸ“ Creating Multi-Project Folders'));
117
- console.log(chalk.gray(' Detected: ' + jiraProjects.length + ' Jira projects (' + jiraProjects.join(', ') + ')'));
118
- for (const projectKey of jiraProjects) {
119
- const projectId = projectKey.toLowerCase();
120
- const specsPath = path.join(targetDir, '.specweave', 'docs', 'internal', 'specs', projectId);
121
- if (!fs.existsSync(specsPath)) {
122
- fs.mkdirSync(specsPath, { recursive: true });
123
- }
124
- console.log(chalk.green(' āœ“ Created project: ' + projectKey + ' (simplified structure)'));
125
- }
126
- console.log('');
127
- }
128
- }
129
- // ADO Multi-Area Folder Creation (reads from config.json, not .env)
130
- // CRITICAL FIX (2025-12-01): Iterate ALL ADO profiles, not just the first one
131
- // Bug: .find() only returned first profile, causing multi-project folders to be skipped
132
- const syncConfig = config.sync;
133
- const profiles = (syncConfig?.profiles || {});
134
- // Filter ALL ADO profiles (not .find() which only returns first!)
135
- const adoProfiles = Object.values(profiles).filter(p => p.provider === 'ado');
136
- if (adoProfiles.length > 0) {
137
- console.log(chalk.blue('\nšŸ“ Creating Azure DevOps Folders'));
138
- for (const adoProfile of adoProfiles) {
139
- if (!adoProfile?.config)
140
- continue;
141
- const { organization, project, areaPaths } = adoProfile.config;
142
- if (organization && project) {
143
- console.log(chalk.gray(` Project: ${project}`));
144
- const projectFolder = project.replace(/\s+/g, '-').toLowerCase();
145
- if (areaPaths?.length) {
146
- // Create folder per area path
147
- for (const areaPath of areaPaths) {
148
- const areaName = areaPath.split('\\').pop() || areaPath;
149
- const areaFolder = areaName.replace(/\s+/g, '-').toLowerCase();
150
- const specsPath = path.join(targetDir, '.specweave', 'docs', 'internal', 'specs', projectFolder, areaFolder);
151
- if (!fs.existsSync(specsPath)) {
152
- fs.mkdirSync(specsPath, { recursive: true });
153
- }
154
- console.log(chalk.green(` āœ“ Created: specs/${projectFolder}/${areaFolder}/`));
155
- }
156
- }
157
- else {
158
- // Single project folder (no area paths)
159
- const specsPath = path.join(targetDir, '.specweave', 'docs', 'internal', 'specs', projectFolder);
160
- if (!fs.existsSync(specsPath)) {
161
- fs.mkdirSync(specsPath, { recursive: true });
162
- }
163
- console.log(chalk.green(` āœ“ Created: specs/${projectFolder}/`));
164
- }
165
- }
166
- }
167
- console.log('');
168
- }
169
- // JIRA Multi-Board Folder Creation (reads from config.json, similar to ADO)
170
- // CRITICAL FIX (2025-12-09): Creates folders from JIRA sync profiles
171
- // Bug: JIRA folders were only created from legacy .env JIRA_PROJECTS, not from config.json profiles
172
- const jiraProfiles = Object.values(profiles).filter(p => p.provider === 'jira');
173
- if (jiraProfiles.length > 0) {
174
- console.log(chalk.blue('\nšŸ“ Creating JIRA Folders'));
175
- for (const jiraProfile of jiraProfiles) {
176
- if (!jiraProfile?.config?.projectKey)
177
- continue;
178
- const { projectKey, boards } = jiraProfile.config;
179
- console.log(chalk.gray(` Project: ${projectKey}`));
180
- const projectFolder = projectKey.toLowerCase().replace(/[^a-z0-9]/g, '-');
181
- if (boards?.length) {
182
- // Create folder per board (2-level structure)
183
- for (const board of boards) {
184
- const boardName = board.name || `board-${board.id}`;
185
- const boardFolder = boardName.toLowerCase().replace(/[^a-z0-9]/g, '-');
186
- const specsPath = path.join(targetDir, '.specweave', 'docs', 'internal', 'specs', projectFolder, boardFolder);
187
- if (!fs.existsSync(specsPath)) {
188
- fs.mkdirSync(specsPath, { recursive: true });
189
- }
190
- console.log(chalk.green(` āœ“ Created: specs/${projectFolder}/${boardFolder}/`));
191
- }
192
- }
193
- else {
194
- // Single project folder (no boards)
195
- const specsPath = path.join(targetDir, '.specweave', 'docs', 'internal', 'specs', projectFolder);
196
- if (!fs.existsSync(specsPath)) {
197
- fs.mkdirSync(specsPath, { recursive: true });
198
- }
199
- console.log(chalk.green(` āœ“ Created: specs/${projectFolder}/`));
200
- }
201
- }
202
- console.log('');
203
- }
204
- }
205
- /**
206
- * Main init command
39
+ * Main init command — simplified to core scaffolding only.
207
40
  */
208
41
  export async function initCommand(projectName, options = {}) {
209
- // Detect CI/non-interactive environment or quick mode (unified, 0188)
210
42
  const isCI = isNonInteractive(options);
211
- // In quick mode, show a brief message
212
43
  if (options.quick) {
213
- console.log(chalk.cyan('\n⚔ Quick mode: Using sensible defaults (local git, no external tools)'));
44
+ console.log(chalk.cyan('\n⚔ Quick mode: Using sensible defaults'));
214
45
  }
215
- // STEP 1: LANGUAGE SELECTION (FIRST QUESTION!)
216
- // This must be asked before anything else so all prompts are in user's language
46
+ // STEP 1: Language selection (FIRST!)
217
47
  let languageResult;
218
- // Validate CLI language option if provided
219
48
  const cliLanguage = options.language?.toLowerCase();
220
49
  if (cliLanguage && !isLanguageSupported(cliLanguage)) {
221
50
  console.error(chalk.red('\nāŒ Invalid language: ' + options.language));
222
51
  console.error(chalk.yellow('Supported languages: ' + getSupportedLanguages().join(', ') + '\n'));
223
52
  process.exit(1);
224
53
  }
225
- // Ask for language (or use CLI option / CI default)
226
54
  if (isCI) {
227
55
  languageResult = getDefaultLanguageSelection(cliLanguage || 'en');
228
56
  }
@@ -234,17 +62,15 @@ export async function initCommand(projectName, options = {}) {
234
62
  }
235
63
  const language = languageResult.language;
236
64
  const locale = getLocaleManager(language);
237
- // Now show welcome message in selected language
238
65
  console.log(chalk.blue.bold('\n' + locale.t('cli', 'init.welcome') + '\n'));
66
+ // STEP 2: Path resolution
239
67
  let targetDir = '';
240
68
  let finalProjectName = '';
241
69
  let usedDotNotation = false;
242
70
  let continueExisting = false;
243
- // Handle "." for current directory
244
71
  if (projectName === '.') {
245
72
  usedDotNotation = true;
246
73
  targetDir = process.cwd();
247
- // Safety: Prevent init in home directory
248
74
  if (path.resolve(targetDir) === path.resolve(os.homedir())) {
249
75
  console.log(chalk.red.bold('\nāŒ DANGEROUS: Cannot initialize SpecWeave in home directory!\n'));
250
76
  console.log(chalk.yellow(' Your home directory contains ALL your projects.'));
@@ -253,121 +79,85 @@ export async function initCommand(projectName, options = {}) {
253
79
  process.exit(1);
254
80
  }
255
81
  const dirName = path.basename(targetDir);
256
- // Validate directory name
257
- if (!/^[a-z0-9-]+$/.test(dirName)) {
82
+ if (!PROJECT_NAME_PATTERN.test(dirName)) {
258
83
  const suggestedName = dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
259
84
  if (isCI) {
260
- console.log(chalk.yellow('\n' + locale.t('cli', 'init.warnings.invalidDirName', { dirName })));
261
- console.log(chalk.gray(' → CI mode: Auto-sanitizing to "' + suggestedName + '"'));
262
85
  finalProjectName = suggestedName;
263
86
  }
264
87
  else {
265
- console.log(chalk.yellow('\n' + locale.t('cli', 'init.warnings.invalidDirName', { dirName })));
266
88
  finalProjectName = await input({
267
89
  message: 'Project name (for templates):',
268
90
  default: suggestedName,
269
- validate: (val) => /^[a-z0-9-]+$/.test(val) || 'Project name must be lowercase letters, numbers, and hyphens only',
91
+ validate: (val) => PROJECT_NAME_PATTERN.test(val) || PROJECT_NAME_VALIDATION_MSG,
270
92
  });
271
93
  }
272
94
  }
273
95
  else {
274
96
  finalProjectName = dirName;
275
97
  }
276
- // Warn if directory not empty
277
- const existingFiles = fs.readdirSync(targetDir).filter(f => !f.startsWith('.'));
278
- if (existingFiles.length > 0 && !options.force) {
279
- if (isCI) {
280
- console.log(chalk.yellow('\n' + locale.t('cli', 'init.warnings.directoryNotEmpty', { count: existingFiles.length, plural: existingFiles.length === 1 ? '' : 's' })));
281
- console.log(chalk.gray(' → CI mode: Proceeding with initialization'));
282
- }
283
- else {
284
- console.log(chalk.yellow('\n' + locale.t('cli', 'init.warnings.directoryNotEmpty', { count: existingFiles.length, plural: existingFiles.length === 1 ? '' : 's' })));
285
- const proceed = await confirm({ message: 'Initialize SpecWeave in current directory?', default: false });
286
- if (!proceed) {
287
- console.log(chalk.yellow(locale.t('cli', 'init.errors.cancelled')));
288
- process.exit(0);
289
- }
290
- }
291
- }
292
- // Smart re-initialization
98
+ // Smart re-init
293
99
  if (fs.existsSync(path.join(targetDir, '.specweave'))) {
294
100
  const result = await promptSmartReinit({ targetDir, isCI, hasForce: !!options.force, language });
295
- if (result.action === 'cancel') {
101
+ if (result.action === 'cancel')
296
102
  process.exit(0);
297
- }
298
103
  continueExisting = result.continueExisting;
299
104
  }
105
+ else {
106
+ // Info: initializing in a non-empty directory
107
+ try {
108
+ const existingFiles = fs.readdirSync(targetDir).filter((f) => !f.startsWith('.'));
109
+ if (existingFiles.length > 0) {
110
+ console.log(chalk.gray(`\n ℹ Directory contains ${existingFiles.length} file(s). Init is non-destructive — only adds .specweave/.\n`));
111
+ }
112
+ }
113
+ catch { /* ignore read errors */ }
114
+ }
300
115
  }
301
116
  else {
302
- // Create subdirectory OR use current directory in quick mode
303
117
  if (!projectName) {
304
118
  if (isCI) {
305
- // CI/quick mode without project name: use current directory (like "." notation)
306
- // This enables: specweave init --quick (without any args)
307
119
  usedDotNotation = true;
308
120
  targetDir = process.cwd();
309
121
  const dirName = path.basename(targetDir);
310
- // Sanitize directory name for project name
311
- if (!/^[a-z0-9-]+$/.test(dirName)) {
312
- finalProjectName = dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
313
- console.log(chalk.gray(` → Quick mode: Using current directory "${dirName}" as "${finalProjectName}"`));
314
- }
315
- else {
316
- finalProjectName = dirName;
317
- console.log(chalk.gray(` → Quick mode: Using current directory "${finalProjectName}"`));
318
- }
319
- // Smart re-initialization for quick mode
122
+ finalProjectName = !PROJECT_NAME_PATTERN.test(dirName)
123
+ ? dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
124
+ : dirName;
320
125
  if (fs.existsSync(path.join(targetDir, '.specweave'))) {
321
126
  const result = await promptSmartReinit({ targetDir, isCI, hasForce: !!options.force, language });
322
- if (result.action === 'cancel') {
127
+ if (result.action === 'cancel')
323
128
  process.exit(0);
324
- }
325
129
  continueExisting = result.continueExisting;
326
130
  }
327
- // Quick mode handled: targetDir and finalProjectName already set
328
- // Skip to nested .specweave check below
329
131
  }
330
132
  else {
331
133
  projectName = await input({
332
134
  message: 'Project name:',
333
135
  default: 'my-saas',
334
- validate: (val) => /^[a-z0-9-]+$/.test(val) || 'Project name must be lowercase letters, numbers, and hyphens only',
136
+ validate: (val) => PROJECT_NAME_PATTERN.test(val) || PROJECT_NAME_VALIDATION_MSG,
335
137
  });
336
138
  }
337
139
  }
338
- // Only process subdirectory creation if projectName was provided/set
339
- // (Quick mode without args already set targetDir and finalProjectName above)
340
140
  if (projectName) {
341
141
  targetDir = path.resolve(process.cwd(), projectName);
342
- // CRITICAL FIX (v1.0.23): Normalize projectName to strip path prefixes like ./
343
- // Bug: "specweave init ./my-project" stored "./my-project" in config.json
344
- // which later caused split('/')[0] to return "." and fail validation
345
142
  finalProjectName = path.basename(projectName);
346
143
  if (fs.existsSync(targetDir)) {
347
144
  const hasSpecweave = fs.existsSync(path.join(targetDir, '.specweave'));
348
145
  if (hasSpecweave) {
349
146
  const result = await promptSmartReinit({ targetDir, isCI, hasForce: !!options.force, language });
350
- if (result.action === 'cancel') {
147
+ if (result.action === 'cancel')
351
148
  process.exit(0);
352
- }
353
149
  continueExisting = result.continueExisting;
354
150
  }
355
151
  else {
356
152
  const existingFiles = fs.readdirSync(targetDir).filter(f => !f.startsWith('.'));
357
153
  if (existingFiles.length > 0) {
358
- console.log(chalk.yellow('\nDirectory ' + projectName + ' exists with ' + existingFiles.length + ' file(s).'));
359
- if (isCI) {
360
- // CI/quick mode: proceed without asking
361
- console.log(chalk.gray(' → CI/quick mode: Proceeding with initialization'));
362
- }
363
- else {
154
+ if (!isCI) {
364
155
  const initExisting = await confirm({ message: 'Initialize SpecWeave in existing directory (non-destructive)?', default: false });
365
156
  if (!initExisting) {
366
157
  console.log(chalk.yellow(locale.t('cli', 'init.errors.cancelled')));
367
158
  process.exit(0);
368
159
  }
369
160
  }
370
- console.log(chalk.green(' āœ… Initializing in existing directory (brownfield-safe)\n'));
371
161
  }
372
162
  }
373
163
  }
@@ -376,88 +166,65 @@ export async function initCommand(projectName, options = {}) {
376
166
  }
377
167
  }
378
168
  }
379
- // Guard: Prevent init inside umbrella sub-repos
169
+ // STEP 3: Guard clauses
380
170
  const umbrellaResult = detectUmbrellaParent(targetDir);
381
171
  if (umbrellaResult) {
382
172
  if (options.force) {
383
- console.log(chalk.yellow(`\nāš ļø Warning: This directory is inside an umbrella project at ${umbrellaResult.umbrellaRoot}`));
384
- console.log(chalk.yellow(' Proceeding because --force was specified.\n'));
173
+ console.log(chalk.yellow(`\nāš ļø Warning: Inside umbrella project at ${umbrellaResult.umbrellaRoot}. Proceeding with --force.\n`));
385
174
  }
386
175
  else {
387
- console.log(chalk.red.bold('\nāŒ Cannot initialize here: this directory is inside an umbrella project.\n'));
176
+ console.log(chalk.red.bold('\nāŒ Cannot initialize here: inside an umbrella project.\n'));
388
177
  console.log(chalk.yellow(` Umbrella root: ${umbrellaResult.umbrellaRoot}`));
389
- console.log(chalk.yellow(` Detected via: ${umbrellaResult.reason === 'config-umbrella-repo' ? 'config.json umbrellaRepo flag' : 'repositories/ directory'}`));
390
178
  console.log(chalk.cyan('\nšŸ’” Run specweave init in the umbrella root instead, or use --force to override.\n'));
391
179
  process.exit(1);
392
180
  }
393
181
  }
394
- // Guard: Prevent init in suspicious paths (node_modules, dist, .git, etc.)
395
182
  const suspiciousResult = detectSuspiciousPath(targetDir);
396
183
  if (suspiciousResult) {
397
184
  if (options.force) {
398
- console.log(chalk.yellow(`\nāš ļø Warning: Path contains suspicious segment "${suspiciousResult.segment}"`));
399
- console.log(chalk.yellow(' Proceeding because --force was specified.\n'));
185
+ console.log(chalk.yellow(`\nāš ļø Warning: Path contains suspicious segment "${suspiciousResult.segment}". Proceeding with --force.\n`));
400
186
  }
401
187
  else {
402
- console.log(chalk.red.bold(`\nāŒ Cannot initialize here: path contains "${suspiciousResult.segment}" which is not a project root.\n`));
188
+ console.log(chalk.red.bold(`\nāŒ Cannot initialize here: path contains "${suspiciousResult.segment}".\n`));
403
189
  console.log(chalk.yellow(` Suggested project root: ${suspiciousResult.suggestedRoot}`));
404
- console.log(chalk.cyan('\nšŸ’” Run specweave init in your project root instead, or use --force to override.\n'));
405
190
  process.exit(1);
406
191
  }
407
192
  }
408
- // Check for nested .specweave/
409
- // EXCEPTION: User-level folders are VALID (e.g., ~/.specweave for global memory/state)
410
- // EXCEPTION: Stale folders (no config.json) are NOT real projects and don't block init
411
193
  const parentSpecweaveFolders = detectNestedSpecweave(targetDir);
412
194
  if (parentSpecweaveFolders && parentSpecweaveFolders.length > 0) {
413
- // Filter out user-level and stale folders
414
195
  const problematicFolders = parentSpecweaveFolders.filter(f => !f.isUserLevel && !f.isStale);
415
196
  const staleFolders = parentSpecweaveFolders.filter(f => f.isStale);
416
- // Warn about stale folders and offer cleanup
417
- if (staleFolders.length > 0) {
418
- console.log(chalk.yellow('\nāš ļø Found stale .specweave/ folder(s) without config.json (not a real project):\n'));
197
+ if (staleFolders.length > 0 && !isCI) {
198
+ console.log(chalk.yellow('\nāš ļø Found stale .specweave/ folder(s) without config.json:\n'));
419
199
  for (const folder of staleFolders) {
420
200
  console.log(chalk.gray(' ' + path.join(folder.path, '.specweave') + '/'));
421
201
  }
422
- if (!isCI) {
423
- const cleanup = await confirm({ message: 'Remove stale .specweave/ folder(s)?', default: true });
424
- if (cleanup) {
425
- for (const folder of staleFolders) {
426
- const stalePath = path.join(folder.path, '.specweave');
427
- try {
428
- fs.rmSync(stalePath, { recursive: true, force: true });
429
- console.log(chalk.green(' āœ… Removed ' + stalePath));
430
- }
431
- catch {
432
- console.log(chalk.yellow(' āš ļø Could not remove ' + stalePath));
433
- }
202
+ const cleanup = await confirm({ message: 'Remove stale .specweave/ folder(s)?', default: true });
203
+ if (cleanup) {
204
+ for (const folder of staleFolders) {
205
+ try {
206
+ fs.rmSync(path.join(folder.path, '.specweave'), { recursive: true, force: true });
434
207
  }
435
- console.log('');
208
+ catch { /* skip */ }
436
209
  }
437
210
  }
438
- else {
439
- console.log(chalk.gray('\n → Ignoring stale folders (no config.json)\n'));
440
- }
441
211
  }
442
212
  if (problematicFolders.length > 0) {
443
213
  console.log(chalk.red.bold('\n' + locale.t('cli', 'init.errors.nestedNotSupported') + '\n'));
444
214
  for (const folder of problematicFolders) {
445
215
  console.log(chalk.yellow(' Found .specweave/ at: ' + folder.path));
446
216
  }
447
- console.log(chalk.cyan.bold('\n šŸ’” SpecWeave doesn\'t support nested projects.'));
448
- console.log(chalk.white(' Initialize in a different directory or remove the parent .specweave/ folder.\n'));
449
217
  process.exit(1);
450
218
  }
451
- // User-level or stale folders found but no problematic ones - allow init to proceed
452
219
  }
220
+ // STEP 4: Create project
453
221
  const spinner = ora('Creating SpecWeave project...').start();
454
222
  try {
455
- // Detect or select tool
223
+ // Adapter detection
456
224
  const adapterLoader = new AdapterLoader();
457
225
  let toolName;
458
226
  if (options.adapter) {
459
227
  toolName = options.adapter;
460
- spinner.text = 'Using ' + toolName + '...';
461
228
  }
462
229
  else {
463
230
  let existingAdapter = null;
@@ -465,104 +232,50 @@ export async function initCommand(projectName, options = {}) {
465
232
  const existingConfigPath = path.join(targetDir, '.specweave', 'config.json');
466
233
  if (fs.existsSync(existingConfigPath)) {
467
234
  try {
468
- const existingConfig = fs.readJsonSync(existingConfigPath);
469
- existingAdapter = existingConfig?.adapters?.default || null;
235
+ existingAdapter = fs.readJsonSync(existingConfigPath)?.adapters?.default || null;
470
236
  }
471
237
  catch { /* ignore */ }
472
238
  }
473
239
  }
474
240
  const detectedTool = await adapterLoader.detectTool();
475
241
  spinner.stop();
476
- console.log(chalk.cyan('\nšŸ” ' + locale.t('cli', 'init.toolDetection.header')));
477
- if (existingAdapter) {
478
- console.log(chalk.blue(' šŸ“‹ Current adapter: ' + existingAdapter));
479
- }
480
- else {
481
- console.log(chalk.gray(' ' + locale.t('cli', 'init.toolDetection.detected', { tool: detectedTool })));
482
- }
483
- console.log('');
484
242
  if (isCI) {
485
- console.log(chalk.gray(' ' + locale.t('cli', 'init.toolDetection.ciAutoConfirm', { tool: detectedTool })));
486
243
  toolName = detectedTool;
487
244
  }
488
245
  else {
489
- // CRITICAL (v1.0.25): Ask user if they want to use Claude.
490
- // If YES → use Claude (CLAUDE.md)
491
- // If NO → use generic (AGENTS.md for all non-Claude tools)
492
- // No need to ask which specific non-Claude tool - AGENTS.md works for all!
246
+ console.log(chalk.cyan('\nšŸ” ' + locale.t('cli', 'init.toolDetection.header')));
247
+ if (existingAdapter) {
248
+ console.log(chalk.blue(' šŸ“‹ Current adapter: ' + existingAdapter));
249
+ }
250
+ else {
251
+ console.log(chalk.gray(' ' + locale.t('cli', 'init.toolDetection.detected', { tool: detectedTool })));
252
+ }
253
+ console.log('');
493
254
  const confirmTool = await confirm({
494
255
  message: locale.t('cli', 'init.toolDetection.confirmPrompt', { tool: detectedTool }),
495
256
  default: true
496
257
  });
258
+ toolName = confirmTool ? detectedTool : 'generic';
497
259
  if (!confirmTool) {
498
- // User declined Claude → use generic adapter (AGENTS.md)
499
- // AGENTS.md is the universal standard for all non-Claude AI tools
500
- toolName = 'generic';
501
260
  console.log(chalk.gray(' → Using AGENTS.md (universal format for non-Claude tools)'));
502
261
  }
503
- else {
504
- toolName = detectedTool;
505
- }
506
262
  }
507
263
  spinner.start('Using ' + toolName + '...');
508
264
  }
509
- // ========================================================================
510
- // PROJECT MATURITY: Greenfield vs Brownfield (v1.0.342)
511
- // Ask BEFORE repository hosting so greenfield users can defer structure.
512
- // ========================================================================
513
- spinner.stop();
514
- let projectMaturity;
515
- const autoDetectedGreenfield = isGreenfieldCheck(targetDir);
516
- if (isCI) {
517
- projectMaturity = autoDetectedGreenfield ? 'greenfield' : 'brownfield';
518
- console.log(chalk.gray(` → Auto-detected: ${projectMaturity} project\n`));
519
- }
520
- else {
521
- console.log(chalk.cyan.bold('\n🌱 Project Type\n'));
522
- projectMaturity = await select({
523
- message: locale.t('cli', 'init.maturity.question'),
524
- choices: [
525
- {
526
- name: `šŸ†• ${locale.t('cli', 'init.maturity.greenfield')} ${chalk.gray('- ' + locale.t('cli', 'init.maturity.greenfieldDesc'))}`,
527
- value: 'greenfield',
528
- },
529
- {
530
- name: `šŸ“¦ ${locale.t('cli', 'init.maturity.brownfield')} ${chalk.gray('- ' + locale.t('cli', 'init.maturity.brownfieldDesc'))}`,
531
- value: 'brownfield',
532
- },
533
- ],
534
- default: autoDetectedGreenfield ? 'greenfield' : 'brownfield',
535
- });
536
- }
537
- spinner.start('Configuring project...');
538
- // ========================================================================
539
- // INTERACTIVE: Repository hosting + umbrella selection (BEFORE file creation)
540
- // v1.0.286: Moved BEFORE directory structure creation so that:
541
- // - Umbrella clone runs on a clean slate (no .specweave/ to conflict with)
542
- // - No git init needed if umbrella provides .git
543
- // - No file merging or .git replacement required
544
- // ========================================================================
545
- spinner.stop();
546
- const gitHubRemote = detectGitHubRemote(targetDir);
547
- const repoResult = await setupRepositoryHosting({ targetDir, isCI, gitHubRemote, language, projectMaturity });
548
- spinner.start('Creating project files...');
265
+ // Provider auto-detection from .git/config (silent, no prompts)
266
+ const providerInfo = detectProvider(targetDir);
549
267
  // Create directory structure
550
268
  if (!continueExisting) {
551
269
  await createDirectoryStructure(targetDir, toolName);
552
270
  spinner.text = 'Directory structure created...';
553
271
  }
554
- else {
555
- spinner.text = 'Using existing directory structure...';
556
- }
557
- // Note: Marketplace registration happens in installAllPlugins or via fallback
558
- // No fake success message here - actual registration is done below
559
272
  // Copy templates
560
273
  const templatesDir = findSourceDir('templates', __dirname);
561
274
  if (!continueExisting) {
562
275
  await copyTemplates(templatesDir, targetDir, finalProjectName, language);
563
- spinner.text = 'Base templates copied...';
276
+ spinner.text = 'Templates copied...';
564
277
  }
565
- // Install based on tool
278
+ // Non-Claude adapter install
566
279
  if (toolName === 'claude') {
567
280
  spinner.text = 'Configuring for Claude Code...';
568
281
  console.log('\n' + locale.t('cli', 'init.claudeNativeComplete'));
@@ -570,7 +283,7 @@ export async function initCommand(projectName, options = {}) {
570
283
  else {
571
284
  await installNonClaudeAdapter(adapterLoader, toolName, targetDir, finalProjectName, options, spinner);
572
285
  }
573
- // Initialize git
286
+ // Git init
574
287
  const gitDir = path.join(targetDir, '.git');
575
288
  if (!fs.existsSync(gitDir)) {
576
289
  const gitInitResult = execFileNoThrowSync('git', ['init'], { cwd: targetDir, shell: false });
@@ -593,13 +306,48 @@ export async function initCommand(projectName, options = {}) {
593
306
  });
594
307
  }
595
308
  }
596
- // Create config.json
597
- createConfigFile(targetDir, finalProjectName, toolName, language, false, undefined, undefined, projectMaturity, repoResult.structureDeferred);
598
- // Auto-install plugins for Claude ONLY
309
+ // Create config.json (simplified — no maturity, no structureDeferred)
310
+ createConfigFile(targetDir, finalProjectName, toolName, language, false);
311
+ const configPath = path.join(targetDir, '.specweave', 'config.json');
312
+ const isGitRepo = fs.existsSync(path.join(targetDir, '.git'));
313
+ // Batch config updates: provider + smart defaults + LSP (single read-modify-write)
314
+ if (fs.existsSync(configPath)) {
315
+ try {
316
+ const config = fs.readJsonSync(configPath);
317
+ // Provider info from .git/config
318
+ if (providerInfo) {
319
+ const org = providerInfo.owner || providerInfo.organization;
320
+ config.repository = {
321
+ provider: providerInfo.provider,
322
+ ...(org && { organization: org }),
323
+ ...(providerInfo.repo && { repo: providerInfo.repo }),
324
+ };
325
+ }
326
+ // Smart defaults
327
+ applySmartDefaults(config, { adapter: toolName, language, isGitRepo });
328
+ if (languageResult.keepEnglishOriginals && config.translation) {
329
+ config.translation.keepEnglishOriginals = true;
330
+ }
331
+ // LSP auto-enable (Claude only)
332
+ if (toolName === 'claude') {
333
+ config.lsp = {
334
+ enabled: true,
335
+ autoInstallPlugins: true,
336
+ marketplace: 'boostvolt/claude-code-lsps',
337
+ };
338
+ }
339
+ fs.writeJsonSync(configPath, config, { spaces: 2 });
340
+ }
341
+ catch (err) {
342
+ console.log(chalk.yellow(' ⚠ Could not update config defaults (non-critical)'));
343
+ }
344
+ }
345
+ // Plugin install (Claude only)
346
+ // CRITICAL FIX (v0.34.6): Skip plugin installation when continuing existing config.
347
+ // Previously, re-running `specweave init .` would deregister all marketplace plugins.
599
348
  let autoInstallSucceeded = false;
600
349
  let marketplaceOnly = false;
601
350
  if (toolName === 'claude') {
602
- // CRITICAL FIX (v0.34.6): Skip plugin installation in "continue existing" mode
603
351
  if (continueExisting) {
604
352
  console.log(chalk.green(' āœ“ Keeping existing plugin configuration'));
605
353
  autoInstallSucceeded = true;
@@ -608,313 +356,61 @@ export async function initCommand(projectName, options = {}) {
608
356
  const result = await installAllPlugins({
609
357
  dirname: __dirname,
610
358
  forceRefresh: options.forceRefresh,
611
- lazyMode: !options.fullInstall, // Lazy mode by default, --full disables it
359
+ lazyMode: !options.fullInstall,
612
360
  });
613
361
  autoInstallSucceeded = result.success;
614
362
  marketplaceOnly = result.marketplaceOnly || false;
615
- // NOTE: sw@specweave is enabled at USER level by plugin-installer.ts
616
- // Do NOT enable it at project level — that would make the core framework
617
- // plugin appear as "Project" scope instead of "User" scope in Claude Code UI.
618
363
  }
619
- // Enable agent teams env var in .claude/settings.json
364
+ // Enable agent teams env var
620
365
  try {
621
366
  const { enableAgentTeamsEnvVar } = await import('../helpers/init/claude-settings-env.js');
622
367
  enableAgentTeamsEnvVar(targetDir);
623
368
  }
624
369
  catch {
625
- // Non-critical - agent teams can be enabled later via `specweave team`
626
- }
627
- }
628
- // Track background job IDs for living docs dependencies
629
- const pendingJobIds = [];
630
- // ADO Repository cloning (for multi-repo setups)
631
- if (repoResult.adoProjectSelection && repoResult.adoClonePatternResult) {
632
- try {
633
- const cloneJobId = await triggerAdoRepoCloning(targetDir, repoResult.adoProjectSelection, repoResult.adoClonePatternResult);
634
- if (cloneJobId) {
635
- pendingJobIds.push(cloneJobId);
636
- }
637
- }
638
- catch (cloneError) {
639
- const msg = cloneError instanceof Error ? cloneError.message : String(cloneError);
640
- console.log(chalk.yellow(`\n āš ļø Background clone failed to start: ${msg}`));
641
- console.log(chalk.gray(' You can clone repositories later with /sw-ado:clone-repos\n'));
642
- }
643
- }
644
- // GitHub Repository cloning (for multi-repo setups)
645
- // Note: umbrella repo is cloned inside setupRepositoryHosting (right after user selects it)
646
- // Exclude umbrella repo from nested cloning (it's already at the project root)
647
- let githubClonedRepos = [];
648
- if (repoResult.githubRepoSelection && repoResult.adoClonePatternResult) {
649
- const excludeRepos = repoResult.umbrellaRepo ? [repoResult.umbrellaRepo] : [];
650
- try {
651
- const cloningResult = await triggerGitHubRepoCloning(targetDir, repoResult.githubRepoSelection, repoResult.adoClonePatternResult, repoResult.gitUrlFormat || 'https', excludeRepos);
652
- if (cloningResult.jobId) {
653
- pendingJobIds.push(cloningResult.jobId);
654
- }
655
- githubClonedRepos = cloningResult.clonedRepos;
656
- }
657
- catch (cloneError) {
658
- const msg = cloneError instanceof Error ? cloneError.message : String(cloneError);
659
- console.log(chalk.yellow(`\n āš ļø Background clone failed to start: ${msg}`));
660
- console.log(chalk.gray(' You can clone repositories later with /sw-github:clone-repos\n'));
661
- }
662
- }
663
- // Bitbucket Repository cloning (for multi-repo setups)
664
- if (repoResult.bitbucketRepoSelection && repoResult.adoClonePatternResult) {
665
- try {
666
- const cloneJobId = await triggerBitbucketRepoCloning(targetDir, repoResult.bitbucketRepoSelection, repoResult.adoClonePatternResult);
667
- if (cloneJobId) {
668
- pendingJobIds.push(cloneJobId);
669
- }
670
- }
671
- catch (cloneError) {
672
- const msg = cloneError instanceof Error ? cloneError.message : String(cloneError);
673
- console.log(chalk.yellow(`\n āš ļø Background clone failed to start: ${msg}`));
674
- console.log(chalk.gray(' You can clone repositories later\n'));
675
- }
676
- }
677
- // Update repository config based on hosting selection
678
- // Bug fix: repository.provider was always "local" and organization was never set
679
- {
680
- const configPath = path.join(targetDir, '.specweave', 'config.json');
681
- if (fs.existsSync(configPath)) {
682
- try {
683
- const config = await fs.readJson(configPath);
684
- const hosting = repoResult.hosting;
685
- if (hosting.startsWith('github')) {
686
- const org = repoResult.githubRepoSelection?.org || gitHubRemote?.owner || config.repository?.organization;
687
- config.repository = {
688
- ...config.repository,
689
- provider: 'github',
690
- ...(org && { organization: org }),
691
- ...(repoResult.umbrellaRepo && { umbrellaRepo: repoResult.umbrellaRepo }),
692
- };
693
- }
694
- else if (hosting.startsWith('ado')) {
695
- const org = repoResult.adoProjectSelection?.org || config.repository?.organization;
696
- config.repository = {
697
- ...config.repository,
698
- provider: 'ado',
699
- ...(org && { organization: org }),
700
- };
701
- }
702
- else if (hosting.startsWith('bitbucket')) {
703
- const org = repoResult.bitbucketRepoSelection?.workspace || config.repository?.organization;
704
- config.repository = {
705
- ...config.repository,
706
- provider: 'bitbucket',
707
- ...(org && { organization: org }),
708
- };
709
- }
710
- // When user selected an umbrella repo, enable umbrella mode
711
- // and register cloned repos as child repos
712
- if (repoResult.umbrellaRepo) {
713
- const org = config.repository?.organization;
714
- config.umbrella = {
715
- enabled: true,
716
- projectName: path.basename(targetDir),
717
- childRepos: githubClonedRepos.map(repoName => ({
718
- id: repoName,
719
- name: repoName,
720
- path: `repositories/${org ? org + '/' : ''}${repoName}`,
721
- prefix: repoName.substring(0, 3).toUpperCase(),
722
- ...(org && {
723
- sync: {
724
- github: { owner: org, repo: repoName },
725
- // Inherit global JIRA/ADO config so child repos don't need manual setup
726
- ...(config.sync?.jira?.projectKey && {
727
- jira: { projectKey: config.sync.jira.projectKey },
728
- }),
729
- ...(config.sync?.ado?.project && {
730
- ado: { organization: config.sync.ado.organization, project: config.sync.ado.project },
731
- }),
732
- },
733
- }),
734
- })),
735
- };
736
- // Copy global sync config as umbrella.sync so umbrella-scoped
737
- // increments route to the umbrella repo, not a child
738
- if (config.sync?.github?.owner && config.sync?.github?.repo) {
739
- config.umbrella.sync = {
740
- github: { owner: config.sync.github.owner, repo: config.sync.github.repo },
741
- };
742
- if (config.sync?.jira?.projectKey) {
743
- config.umbrella.sync.jira = { projectKey: config.sync.jira.projectKey };
744
- }
745
- if (config.sync?.ado?.project) {
746
- config.umbrella.sync.ado = { project: config.sync.ado.project };
747
- }
748
- }
749
- }
750
- await fs.writeJson(configPath, config, { spaces: 2 });
751
- }
752
- catch {
753
- // Non-critical — config update failed but init can continue
754
- }
755
- }
756
- }
757
- // Create multi-project folders for GitHub repos (0188: provider symmetry)
758
- if (githubClonedRepos.length > 0) {
759
- console.log(chalk.blue('\nšŸ“ Creating GitHub Project Folders'));
760
- const created = await createProjectFolders(targetDir, githubClonedRepos);
761
- for (const folder of created) {
762
- console.log(chalk.green(` āœ“ Created: specs/${folder}/`));
763
- }
764
- console.log('');
765
- }
766
- // Issue tracker setup
767
- // Skip when greenfield user deferred structure — they'll configure via /sw:sync-setup
768
- let externalPluginInstalled;
769
- if (repoResult.structureDeferred) {
770
- console.log(chalk.gray('\nā­ļø Skipping issue tracker setup (deferred with repository structure)'));
771
- console.log(chalk.gray(' Configure later via /sw:sync-setup\n'));
772
- }
773
- else {
774
- const isFrameworkRepo = await isSpecWeaveFrameworkRepo(targetDir);
775
- const githubRepoSelection = repoResult.githubRepoSelection
776
- ? {
777
- org: repoResult.githubRepoSelection.org,
778
- pat: repoResult.githubRepoSelection.pat,
779
- clonedRepos: githubClonedRepos
780
- }
781
- : undefined;
782
- await setupIssueTrackerWrapper(targetDir, language, isFrameworkRepo, repoResult.hosting, isCI, repoResult.adoProjectSelection, githubRepoSelection, repoResult.gitUrlFormat);
783
- // SMART PLUGIN INSTALL (v1.0.122): Auto-install selected external tool plugin
784
- if (toolName === 'claude' && autoInstallSucceeded) {
785
- externalPluginInstalled = await autoInstallSelectedExternalPlugin(targetDir);
786
- }
787
- }
788
- // Multi-project folders
789
- await createMultiProjectFolders(targetDir);
790
- // ========================================================================
791
- // PHASE: Apply Smart Defaults (0200 — replaces wizard loop)
792
- // No more testing/interview/quality-gates/translation prompts.
793
- // Everything is auto-provisioned and shown in the summary banner.
794
- // ========================================================================
795
- {
796
- const smartDefaultsConfigPath = path.join(targetDir, '.specweave', 'config.json');
797
- if (fs.existsSync(smartDefaultsConfigPath)) {
798
- const config = fs.readJsonSync(smartDefaultsConfigPath);
799
- const isGitRepo = fs.existsSync(path.join(targetDir, '.git'));
800
- applySmartDefaults(config, { adapter: toolName, language, isGitRepo });
801
- // Preserve keepEnglishOriginals from language selection
802
- if (languageResult.keepEnglishOriginals && config.translation) {
803
- config.translation.keepEnglishOriginals = true;
804
- }
805
- fs.writeJsonSync(smartDefaultsConfigPath, config, { spaces: 2 });
370
+ console.log(chalk.yellow(' ⚠ Could not enable agent teams env var (non-critical)'));
806
371
  }
372
+ setupLspEnvVar();
807
373
  }
808
- // ========================================================================
809
- // PHASE: Auto-install Git Hooks (0200 — no prompt)
810
- // ========================================================================
811
- const isGitRepo = fs.existsSync(path.join(targetDir, '.git'));
374
+ // Git hooks
812
375
  if (isGitRepo && !continueExisting) {
813
376
  installGitHooks(targetDir, templatesDir);
814
377
  }
815
- // ========================================================================
816
- // PHASE: Auto-enable LSP for Claude (0200 — no prompt)
817
- // ========================================================================
818
- if (toolName === 'claude') {
819
- const lspConfigPath = path.join(targetDir, '.specweave', 'config.json');
820
- if (fs.existsSync(lspConfigPath)) {
821
- try {
822
- const lspConfig = await fs.readJson(lspConfigPath);
823
- lspConfig.lsp = {
824
- enabled: true,
825
- autoInstallPlugins: true,
826
- marketplace: 'boostvolt/claude-code-lsps',
827
- };
828
- await fs.writeJson(lspConfigPath, lspConfig, { spaces: 2 });
829
- }
830
- catch { /* Non-critical */ }
831
- }
832
- setupLspEnvVar();
833
- }
834
- // ========================================================================
835
- // PHASE: Greenfield Detection + Summary Banner (0200)
836
- // ========================================================================
378
+ // Summary banner
837
379
  {
838
- const greenfieldStatus = isGreenfieldCheck(targetDir);
839
- // Build provider info for banner
840
- let bannerProvider;
841
- if (repoResult.hosting.startsWith('github')) {
842
- bannerProvider = { name: 'GitHub', owner: gitHubRemote?.owner, repo: gitHubRemote?.repo };
843
- }
844
- else if (repoResult.hosting.startsWith('ado')) {
845
- bannerProvider = { name: 'Azure DevOps', organization: repoResult.adoProjectSelection?.org };
846
- }
847
- else if (repoResult.hosting.startsWith('bitbucket')) {
848
- bannerProvider = { name: 'Bitbucket' };
849
- }
850
- else {
851
- bannerProvider = { name: 'Local' };
852
- }
853
- // Single config read for banner data
854
- const bannerConfigPath = path.join(targetDir, '.specweave', 'config.json');
855
380
  let bannerConfig;
856
- if (fs.existsSync(bannerConfigPath)) {
381
+ if (fs.existsSync(configPath)) {
857
382
  try {
858
- bannerConfig = fs.readJsonSync(bannerConfigPath);
383
+ bannerConfig = fs.readJsonSync(configPath);
859
384
  }
860
385
  catch { /* ignore */ }
861
386
  }
862
- // Read tracker from config
863
- let bannerTracker;
864
- if (bannerConfig) {
865
- const syncCfg = bannerConfig.sync;
866
- if (syncCfg?.defaultProfile && syncCfg?.profiles) {
867
- const profile = syncCfg.profiles[syncCfg.defaultProfile];
868
- if (profile?.provider === 'github')
869
- bannerTracker = { name: 'GitHub Issues' };
870
- else if (profile?.provider === 'jira')
871
- bannerTracker = { name: 'Jira' };
872
- else if (profile?.provider === 'ado')
873
- bannerTracker = { name: 'Azure DevOps Boards' };
874
- }
875
- }
876
- // Read sync permissions
877
- let syncPermissions;
878
- if (bannerConfig?.sync?.settings) {
879
- const s = bannerConfig.sync.settings;
880
- syncPermissions = {
881
- canCreate: !!s.canUpsertInternalItems,
882
- canUpdate: !!s.canUpdateExternalItems,
883
- canUpdateStatus: !!s.canUpdateStatus,
387
+ let bannerProvider;
388
+ if (providerInfo) {
389
+ const providerNames = { github: 'GitHub', ado: 'Azure DevOps', bitbucket: 'Bitbucket' };
390
+ bannerProvider = {
391
+ name: providerNames[providerInfo.provider] || providerInfo.provider,
392
+ owner: providerInfo.owner,
393
+ repo: providerInfo.repo,
394
+ organization: providerInfo.organization,
884
395
  };
885
396
  }
886
- // Read defaults (including coverage targets)
887
- let finalDefaults = {
888
- testing: 'TDD',
889
- qualityGates: 'standard',
890
- lspEnabled: toolName === 'claude',
397
+ else {
398
+ bannerProvider = { name: 'Local' };
399
+ }
400
+ const finalDefaults = {
401
+ testing: bannerConfig?.testing?.defaultTestMode || 'TDD',
402
+ qualityGates: bannerConfig?.qualityGates?.preset || 'standard',
403
+ lspEnabled: !!bannerConfig?.lsp?.enabled,
891
404
  gitHooksInstalled: isGitRepo,
892
- translationEnabled: false,
405
+ translationEnabled: !!bannerConfig?.translation?.enabled,
406
+ coverageTargets: bannerConfig?.testing?.coverageTargets,
893
407
  };
894
- if (bannerConfig) {
895
- finalDefaults = {
896
- testing: bannerConfig.testing?.defaultTestMode || 'TDD',
897
- qualityGates: bannerConfig.qualityGates?.preset || 'standard',
898
- lspEnabled: !!bannerConfig.lsp?.enabled,
899
- gitHooksInstalled: isGitRepo,
900
- translationEnabled: !!bannerConfig.translation?.enabled,
901
- coverageTargets: bannerConfig.testing?.coverageTargets,
902
- };
903
- }
904
408
  displaySummaryBanner({
905
409
  projectName: finalProjectName,
906
410
  provider: bannerProvider,
907
- tracker: bannerTracker,
908
- repoCount: 1 + githubClonedRepos.length,
909
- isGreenfield: greenfieldStatus,
910
- hasPendingClones: pendingJobIds.length > 0,
911
411
  adapter: toolName,
912
412
  language,
913
413
  defaults: finalDefaults,
914
- externalPluginInstalled,
915
- syncPermissions,
916
- projectMaturity,
917
- structureDeferred: repoResult.structureDeferred,
918
414
  });
919
415
  }
920
416
  showNextSteps(finalProjectName, toolName, language, usedDotNotation, toolName === 'claude' ? { pluginAutoInstalled: autoInstallSucceeded, marketplaceOnly } : undefined);
@@ -973,106 +469,4 @@ async function installNonClaudeAdapter(adapterLoader, toolName, targetDir, proje
973
469
  spinner.warn('Could not install core plugin');
974
470
  }
975
471
  }
976
- /**
977
- * Wrapper for issue tracker setup
978
- */
979
- async function setupIssueTrackerWrapper(targetDir, language, isFrameworkRepo, repositoryHosting, isCI, adoProjectSelection, githubCredentialsFromRepoSetup, gitUrlFormat) {
980
- try {
981
- const { setupIssueTracker } = await import('../helpers/issue-tracker/index.js');
982
- const configPath = path.join(targetDir, '.specweave', 'config.json');
983
- let existingTracker = null;
984
- if (fs.existsSync(configPath)) {
985
- const config = await fs.readJson(configPath);
986
- if (config.sync?.defaultProfile && config.sync?.profiles) {
987
- const defaultProfile = config.sync.profiles[config.sync.defaultProfile];
988
- existingTracker = defaultProfile?.provider || null;
989
- }
990
- }
991
- if (existingTracker) {
992
- console.log(chalk.blue('\nšŸ” Existing Issue Tracker Configuration Detected'));
993
- console.log(chalk.gray(' Current: ' + existingTracker.charAt(0).toUpperCase() + existingTracker.slice(1)));
994
- if (isCI) {
995
- console.log(chalk.gray(' → CI mode: Keeping existing configuration\n'));
996
- return;
997
- }
998
- const reconfigure = await confirm({ message: 'Do you want to reconfigure your issue tracker?', default: false });
999
- if (!reconfigure) {
1000
- console.log(chalk.gray(' āœ“ Keeping existing configuration\n'));
1001
- return;
1002
- }
1003
- }
1004
- await setupIssueTracker({
1005
- projectPath: targetDir,
1006
- language,
1007
- maxRetries: 3,
1008
- isFrameworkRepo,
1009
- repositoryHosting,
1010
- adoCredentialsFromRepoSetup: adoProjectSelection,
1011
- githubCredentialsFromRepoSetup,
1012
- gitUrlFormat,
1013
- isCI
1014
- });
1015
- }
1016
- catch {
1017
- console.log(chalk.yellow('\nāš ļø Issue tracker setup skipped (can configure later)'));
1018
- }
1019
- }
1020
- /**
1021
- * Auto-install external tool plugin based on issue tracker selection
1022
- *
1023
- * NEW (v1.0.122): Smart plugin installation
1024
- * Instead of loading all 24 plugins (~60K tokens), we:
1025
- * 1. Load router skill by default (~500 tokens)
1026
- * 2. Auto-load ONLY the selected external tool plugin (~5K tokens)
1027
- * 3. Other plugins load on-demand via keywords
1028
- *
1029
- * Result: ~30K max instead of ~60K (50% token savings)
1030
- */
1031
- async function autoInstallSelectedExternalPlugin(targetDir) {
1032
- const configPath = path.join(targetDir, '.specweave', 'config.json');
1033
- if (!fs.existsSync(configPath)) {
1034
- return false;
1035
- }
1036
- try {
1037
- const config = await fs.readJson(configPath);
1038
- const syncConfig = config.sync;
1039
- if (!syncConfig?.defaultProfile || !syncConfig?.profiles) {
1040
- return false;
1041
- }
1042
- const defaultProfile = syncConfig.profiles[syncConfig.defaultProfile];
1043
- if (!defaultProfile?.provider) {
1044
- return false;
1045
- }
1046
- // Map provider to plugin name
1047
- const providerToPlugin = {
1048
- github: 'specweave-github',
1049
- jira: 'specweave-jira',
1050
- ado: 'specweave-ado',
1051
- };
1052
- const pluginToInstall = providerToPlugin[defaultProfile.provider];
1053
- if (!pluginToInstall) {
1054
- return false;
1055
- }
1056
- // Install plugin via Claude CLI directly (v1.0.210 - removed PluginCacheManager)
1057
- console.log(chalk.cyan(`\nšŸ“¦ Auto-installing ${defaultProfile.provider.toUpperCase()} plugin...`));
1058
- const scopeArgs = getScopeArgs(getPluginScope(pluginToInstall, 'specweave'));
1059
- const cliResult = execFileNoThrowSync('claude', ['plugin', 'install', `${pluginToInstall}@specweave`, ...scopeArgs]);
1060
- if (cliResult.success) {
1061
- console.log(chalk.green(` āœ“ ${pluginToInstall} installed (ready for sync commands)`));
1062
- return true;
1063
- }
1064
- else {
1065
- console.log(chalk.yellow(` ⚠ Could not auto-install ${pluginToInstall}`));
1066
- console.log(chalk.gray(` → Install manually: claude plugin install ${pluginToInstall}@specweave`));
1067
- return false;
1068
- }
1069
- }
1070
- catch (error) {
1071
- // Non-blocking - just log and continue
1072
- if (process.env.DEBUG) {
1073
- console.log(chalk.gray(` → Auto-install skipped: ${error instanceof Error ? error.message : String(error)}`));
1074
- }
1075
- return false;
1076
- }
1077
- }
1078
472
  //# sourceMappingURL=init.js.map