chati-dev 3.2.5 → 3.3.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.
Files changed (98) hide show
  1. package/LICENSE +96 -0
  2. package/bin/chati.js +46 -0
  3. package/framework/agents/build/dev.md +122 -1
  4. package/framework/agents/deploy/devops.md +128 -3
  5. package/framework/agents/discover/brief.md +77 -15
  6. package/framework/agents/discover/brownfield-wu.md +2 -2
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +3 -3
  10. package/framework/agents/plan/phases.md +127 -2
  11. package/framework/agents/plan/tasks.md +127 -2
  12. package/framework/agents/plan/ux.md +269 -22
  13. package/framework/agents/quality/qa-implementation.md +172 -8
  14. package/framework/agents/quality/qa-planning.md +147 -2
  15. package/framework/config.yaml +9 -5
  16. package/framework/constitution.md +7 -1
  17. package/framework/context/quality.md +1 -1
  18. package/framework/context/root.md +1 -1
  19. package/framework/hooks/constitution-guard.js +18 -2
  20. package/framework/hooks/mode-governance.js +3 -3
  21. package/framework/hooks/read-protection.js +10 -2
  22. package/framework/i18n/en.yaml +6 -0
  23. package/framework/i18n/es.yaml +6 -0
  24. package/framework/i18n/fr.yaml +6 -0
  25. package/framework/i18n/pt.yaml +6 -0
  26. package/framework/orchestrator/chati.md +102 -6
  27. package/framework/schemas/task.schema.json +1 -1
  28. package/framework/tasks/architect-dep-audit.md +128 -0
  29. package/framework/tasks/architect-stack-selection.md +28 -0
  30. package/framework/workflows/brownfield-fullstack.yaml +2 -2
  31. package/framework/workflows/brownfield-service.yaml +2 -2
  32. package/framework/workflows/brownfield-ui.yaml +2 -2
  33. package/framework/workflows/greenfield-fullstack.yaml +6 -2
  34. package/framework/workflows/quick-flow.yaml +7 -5
  35. package/framework/workflows/standard-flow.yaml +171 -0
  36. package/package.json +4 -2
  37. package/src/api/index.js +129 -0
  38. package/src/autonomy/build-loop.js +93 -6
  39. package/src/autonomy/build-state.js +20 -2
  40. package/src/autonomy/cause-analyzer.js +177 -0
  41. package/src/autonomy/escalation.js +214 -0
  42. package/src/autonomy/safety-net.js +23 -5
  43. package/src/autonomy/worktree-manager.js +245 -0
  44. package/src/config/agent-customizer.js +227 -0
  45. package/src/config/ide-configs.js +57 -27
  46. package/src/decision/analyzer.js +148 -0
  47. package/src/decision/registry-healer.js +38 -21
  48. package/src/extensions/loader.js +151 -0
  49. package/src/extensions/registry.js +134 -0
  50. package/src/gates/circuit-breaker.js +32 -0
  51. package/src/gates/g3-implementation.js +30 -4
  52. package/src/gates/g4-qa-implementation.js +34 -5
  53. package/src/gates/gate-base.js +9 -0
  54. package/src/health/auto-fix.js +216 -0
  55. package/src/installer/core.js +24 -11
  56. package/src/installer/provider-overlay.js +82 -0
  57. package/src/installer/templates.js +22 -10
  58. package/src/installer/transaction.js +3 -2
  59. package/src/installer/validator.js +74 -0
  60. package/src/intelligence/context-status.js +9 -5
  61. package/src/intelligence/document-sharder.js +221 -0
  62. package/src/intelligence/elicitation.js +265 -0
  63. package/src/intelligence/timeline.js +5 -0
  64. package/src/memory/gotchas.js +78 -2
  65. package/src/merger/semantic-merger.js +292 -0
  66. package/src/orchestrator/agent-selector.js +20 -0
  67. package/src/orchestrator/handoff-engine.js +77 -0
  68. package/src/orchestrator/index.js +0 -8
  69. package/src/orchestrator/intent-classifier.js +182 -0
  70. package/src/orchestrator/pipeline-manager.js +125 -1
  71. package/src/orchestrator/session-manager.js +164 -2
  72. package/src/quality/metrics-collector.js +283 -0
  73. package/src/quality/test-runner.js +368 -0
  74. package/src/telemetry/collector.js +83 -0
  75. package/src/telemetry/config.js +119 -0
  76. package/src/telemetry/index.js +11 -0
  77. package/src/telemetry/schema.js +104 -0
  78. package/src/telemetry/sender.js +60 -0
  79. package/src/terminal/cli-registry.js +7 -1
  80. package/src/terminal/cost-tracker.js +197 -0
  81. package/src/terminal/handoff-parser.js +61 -4
  82. package/src/terminal/prompt-builder.js +56 -18
  83. package/src/terminal/rate-limiter.js +172 -0
  84. package/src/terminal/run-agent.js +39 -0
  85. package/src/terminal/run-parallel.js +22 -1
  86. package/src/terminal/spawner.js +181 -3
  87. package/src/upgrade/migrator.js +2 -2
  88. package/src/utils/event-bus.js +126 -0
  89. package/src/utils/file-lock.js +291 -0
  90. package/src/utils/schema-validator.js +226 -0
  91. package/src/wizard/i18n.js +11 -0
  92. package/src/wizard/index.js +42 -20
  93. package/src/wizard/questions.js +200 -39
  94. package/src/autonomy/execution-profile.js +0 -151
  95. package/src/intelligence/file-tracker.js +0 -117
  96. package/src/memory/gotchas-auto-capture.js +0 -253
  97. package/src/orchestrator/pipeline-state.js +0 -223
  98. package/src/terminal/wave-analyzer.js +0 -143
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @fileoverview Lightweight schema validation for runtime data.
3
+ *
4
+ * Validates session.yaml, build-state.json, config.yaml, and handoff
5
+ * blocks against defined schemas. No external dependencies.
6
+ *
7
+ * Constitution Article XIV — Framework Registry Governance.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Validation Engine
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * @typedef {object} SchemaProperty
16
+ * @property {string} type - Expected type (string|number|boolean|array|object)
17
+ * @property {boolean} [required] - Whether field is required
18
+ * @property {*} [default] - Default value if missing
19
+ * @property {*[]} [enum] - Allowed values
20
+ * @property {number} [min] - Minimum value (for numbers)
21
+ * @property {number} [max] - Maximum value (for numbers)
22
+ * @property {number} [minLength] - Minimum length (for strings/arrays)
23
+ * @property {number} [maxLength] - Maximum length (for strings/arrays)
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} Schema
28
+ * @property {string[]} [required] - Required field names
29
+ * @property {Record<string, SchemaProperty>} properties - Property definitions
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} ValidationResult
34
+ * @property {boolean} valid - Whether data passes validation
35
+ * @property {string[]} errors - List of validation errors
36
+ * @property {string[]} warnings - List of validation warnings
37
+ */
38
+
39
+ /**
40
+ * Validate data against a schema.
41
+ *
42
+ * @param {object} data - Data to validate
43
+ * @param {Schema} schema - Schema definition
44
+ * @returns {ValidationResult}
45
+ */
46
+ export function validateSchema(data, schema) {
47
+ const errors = [];
48
+ const warnings = [];
49
+
50
+ if (!data || typeof data !== 'object') {
51
+ errors.push('Data must be a non-null object');
52
+ return { valid: false, errors, warnings };
53
+ }
54
+
55
+ if (!schema || !schema.properties) {
56
+ return { valid: true, errors, warnings };
57
+ }
58
+
59
+ // Check required fields
60
+ const requiredFields = schema.required || [];
61
+ for (const field of requiredFields) {
62
+ if (data[field] === undefined || data[field] === null) {
63
+ errors.push(`Missing required field: ${field}`);
64
+ }
65
+ }
66
+
67
+ // Validate each defined property
68
+ for (const [key, prop] of Object.entries(schema.properties)) {
69
+ const value = data[key];
70
+
71
+ // Skip undefined/null non-required fields
72
+ if (value === undefined || (value === null && !requiredFields.includes(key))) {
73
+ continue;
74
+ }
75
+
76
+ // Type check
77
+ if (prop.type) {
78
+ const actual = getType(value);
79
+ if (actual !== prop.type) {
80
+ errors.push(`Field "${key}": expected ${prop.type}, got ${actual}`);
81
+ continue;
82
+ }
83
+ }
84
+
85
+ // Enum check
86
+ if (prop.enum && !prop.enum.includes(value)) {
87
+ errors.push(`Field "${key}": value "${value}" not in allowed values [${prop.enum.join(', ')}]`);
88
+ }
89
+
90
+ // Number range
91
+ if (prop.type === 'number') {
92
+ if (prop.min !== undefined && value < prop.min) {
93
+ errors.push(`Field "${key}": value ${value} below minimum ${prop.min}`);
94
+ }
95
+ if (prop.max !== undefined && value > prop.max) {
96
+ errors.push(`Field "${key}": value ${value} above maximum ${prop.max}`);
97
+ }
98
+ }
99
+
100
+ // String/Array length
101
+ if (prop.type === 'string' || prop.type === 'array') {
102
+ const len = value.length;
103
+ if (prop.minLength !== undefined && len < prop.minLength) {
104
+ warnings.push(`Field "${key}": length ${len} below recommended minimum ${prop.minLength}`);
105
+ }
106
+ if (prop.maxLength !== undefined && len > prop.maxLength) {
107
+ warnings.push(`Field "${key}": length ${len} above recommended maximum ${prop.maxLength}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ return { valid: errors.length === 0, errors, warnings };
113
+ }
114
+
115
+ /**
116
+ * Validate data and apply default values for missing fields.
117
+ *
118
+ * @param {object} data - Data to validate (will NOT be mutated)
119
+ * @param {Schema} schema - Schema definition
120
+ * @returns {{ valid: boolean, data: object, errors: string[], warnings: string[] }}
121
+ */
122
+ export function validateAndCoerce(data, schema) {
123
+ const result = validateSchema(data, schema);
124
+
125
+ // Create a copy with defaults applied
126
+ const coerced = { ...data };
127
+
128
+ if (schema.properties) {
129
+ for (const [key, prop] of Object.entries(schema.properties)) {
130
+ if (coerced[key] === undefined && prop.default !== undefined) {
131
+ coerced[key] = prop.default;
132
+ }
133
+ }
134
+ }
135
+
136
+ return {
137
+ valid: result.valid,
138
+ data: coerced,
139
+ errors: result.errors,
140
+ warnings: result.warnings,
141
+ };
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Helpers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Get the type of a value (matching schema type names).
150
+ *
151
+ * @param {*} value
152
+ * @returns {string}
153
+ */
154
+ function getType(value) {
155
+ if (Array.isArray(value)) return 'array';
156
+ if (value === null) return 'null';
157
+ return typeof value;
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Built-in Schemas
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /**
165
+ * Session schema (session.yaml).
166
+ */
167
+ export const SESSION_SCHEMA = {
168
+ required: ['project', 'language'],
169
+ properties: {
170
+ project: { type: 'string', required: true, minLength: 1 },
171
+ language: { type: 'string', required: true, default: 'en' },
172
+ pipeline_phase: { type: 'string', enum: ['discover', 'plan', 'build', 'deploy', 'completed'] },
173
+ current_agent: { type: 'string' },
174
+ governance_mode: { type: 'string', enum: ['planning', 'build', 'deploy'] },
175
+ execution_profile: { type: 'string', enum: ['explore', 'guided', 'autonomous'] },
176
+ context_bracket: { type: 'string', enum: ['FRESH', 'MODERATE', 'DEPLETED', 'CRITICAL'] },
177
+ },
178
+ };
179
+
180
+ /**
181
+ * Build state schema (build-state.json).
182
+ */
183
+ export const BUILD_STATE_SCHEMA = {
184
+ required: ['sessionId', 'status', 'checkpoints'],
185
+ properties: {
186
+ sessionId: { type: 'string', required: true },
187
+ status: { type: 'string', required: true, enum: ['idle', 'running', 'completed', 'failed', 'abandoned'] },
188
+ checkpoints: { type: 'array', required: true },
189
+ startedAt: { type: 'string' },
190
+ totalAttempts: { type: 'number', min: 0 },
191
+ },
192
+ };
193
+
194
+ /**
195
+ * Config schema (config.yaml).
196
+ */
197
+ export const CONFIG_SCHEMA = {
198
+ required: ['version'],
199
+ properties: {
200
+ version: { type: 'string', required: true },
201
+ installed_at: { type: 'string' },
202
+ updated_at: { type: 'string' },
203
+ installer_version: { type: 'string' },
204
+ project_type: { type: 'string', enum: ['greenfield', 'brownfield'] },
205
+ language: { type: 'string', default: 'en' },
206
+ ides: { type: 'array' },
207
+ providers: { type: 'object' },
208
+ agent_overrides: { type: 'object' },
209
+ },
210
+ };
211
+
212
+ /**
213
+ * Handoff schema (parsed handoff block).
214
+ */
215
+ export const HANDOFF_SCHEMA = {
216
+ required: ['status'],
217
+ properties: {
218
+ status: { type: 'string', required: true, enum: ['APPROVED', 'NEEDS_REVISION', 'BLOCKED', 'unknown'] },
219
+ score: { type: 'number', min: 0, max: 100 },
220
+ summary: { type: 'string', maxLength: 2000 },
221
+ outputs: { type: 'array' },
222
+ decisions: { type: 'object' },
223
+ blockers: { type: 'array' },
224
+ needs_input_question: { type: 'string' },
225
+ },
226
+ };
@@ -26,6 +26,14 @@ const FALLBACK_EN = {
26
26
  llm_provider_codex: 'Codex (OpenAI) — Optimized for code generation',
27
27
  llm_provider_not_installed: '(CLI not detected)',
28
28
  llm_provider_label: 'AI Provider',
29
+ ide_selection_title: 'Select your IDEs / CLIs (multiple allowed):',
30
+ provider_selection_title: 'Which AI providers will you use?',
31
+ editor_selection_title: 'Which editor IDEs should get rules files? (optional)',
32
+ primary_provider_title: 'Which is your primary CLI provider?',
33
+ primary_provider_label: 'Primary Provider',
34
+ providers_label: 'Providers',
35
+ quick_start_switch_hint: 'Switch CLIs anytime — your session continues from where you left off',
36
+ created_overlays: 'Created provider overlay directories',
29
37
  will_install: 'Will install:',
30
38
  agents_count: '13 agent definitions (DISCOVER, PLAN, BUILD, DEPLOY phases)',
31
39
  workflows_count: '6 workflow blueprints',
@@ -58,6 +66,9 @@ const FALLBACK_EN = {
58
66
  quick_start_1: 'Open your IDE',
59
67
  quick_start_2: 'Type: /chati',
60
68
  quick_start_3: 'The orchestrator will guide you through the process',
69
+ telemetry_consent: 'Help improve Chati.dev by sharing anonymous usage data?',
70
+ telemetry_description: 'Anonymous metrics only (agents used, pipeline duration, gate scores). Zero code, files, or personal data. You can disable anytime with: npx chati-dev telemetry disable',
71
+ telemetry_privacy: 'Privacy: opt-in only, anonymous UUID, no PII collected.',
61
72
  },
62
73
  agents: {
63
74
  starting: 'Starting agent: {agent}',
@@ -3,23 +3,13 @@ import { readFileSync } from 'fs';
3
3
  import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
- import { stepLanguage, stepProjectType, stepLlmProvider, stepConfirmation } from './questions.js';
6
+ import { stepLanguage, stepProjectType, stepIDESelection, stepProviderSelection, stepEditorSelection, stepPrimaryProvider, stepConfirmation, stepTelemetryConsent } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { validateInstallation } from '../installer/validator.js';
10
10
  import { t } from './i18n.js';
11
11
  import { DEFAULT_MCPS } from '../config/mcp-configs.js';
12
- import { IDE_CONFIGS } from '../config/ide-configs.js';
13
-
14
- /**
15
- * Map LLM provider to its primary IDE.
16
- * Each provider maps to its own standalone IDE — no cross-contamination.
17
- */
18
- const PROVIDER_TO_IDE = {
19
- claude: ['claude-code'],
20
- gemini: ['gemini-cli'],
21
- codex: ['codex-cli'],
22
- };
12
+ import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
23
13
 
24
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
15
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
@@ -46,11 +36,25 @@ export async function runWizard(targetDir, options = {}) {
46
36
  // Step 2: Project Type
47
37
  const projectType = options.projectType || await stepProjectType(targetDir);
48
38
 
49
- // Step 3: LLM Provider
50
- const llmProvider = options.llmProvider || await stepLlmProvider();
39
+ // Step 3a: Provider Selection (which AI providers to use)
40
+ const selectedProviders = options.providers || await stepProviderSelection();
41
+
42
+ // Step 3b: Primary provider (only if multiple)
43
+ const primaryProvider = options.llmProvider || await stepPrimaryProvider(selectedProviders);
44
+
45
+ // Step 3c: Editor Selection (which editor IDEs get rules files)
46
+ const selectedEditors = options.editors || await stepEditorSelection();
47
+
48
+ // Combine providers + editors into selectedIDEs for backward compatibility
49
+ const providerToIDE = { claude: 'claude-code', gemini: 'gemini-cli', codex: 'codex-cli' };
50
+ const selectedIDEs = options.ides || [
51
+ ...selectedProviders.map(p => providerToIDE[p]).filter(Boolean),
52
+ ...selectedEditors,
53
+ ];
54
+
55
+ // CLI providers derived from selected providers
56
+ const cliProviders = selectedProviders;
51
57
 
52
- // Map provider to its standalone IDE(s)
53
- const selectedIDEs = options.ides || PROVIDER_TO_IDE[llmProvider] || ['claude-code'];
54
58
  const selectedMCPs = options.mcps || DEFAULT_MCPS;
55
59
 
56
60
  // Step 4: Confirmation
@@ -59,7 +63,8 @@ export async function runWizard(targetDir, options = {}) {
59
63
  projectName,
60
64
  projectType,
61
65
  language,
62
- llmProvider,
66
+ llmProvider: primaryProvider,
67
+ allProviders: cliProviders,
63
68
  selectedIDEs,
64
69
  selectedMCPs,
65
70
  targetDir,
@@ -68,8 +73,12 @@ export async function runWizard(targetDir, options = {}) {
68
73
 
69
74
  await stepConfirmation(config);
70
75
 
76
+ // Step 5: Telemetry Consent (opt-in)
77
+ const telemetryEnabled = options.telemetry !== undefined ? options.telemetry : await stepTelemetryConsent();
78
+ config.telemetryEnabled = telemetryEnabled;
79
+
71
80
  // Step 5: Installation + Validation
72
- const primaryIDE = selectedIDEs[selectedIDEs.length - 1];
81
+ const primaryIDE = selectedIDEs.find(ide => IDE_TO_PROVIDER[ide] === primaryProvider) || selectedIDEs[0];
73
82
  const primaryIDEName = IDE_CONFIGS[primaryIDE]?.name || primaryIDE;
74
83
 
75
84
  console.log();
@@ -97,9 +106,15 @@ export async function runWizard(targetDir, options = {}) {
97
106
  if (selectedIDEs.includes('claude-code')) {
98
107
  showStep(t('installer.created_claude_md'));
99
108
  }
109
+ if (cliProviders.length > 1) {
110
+ showStep(t('installer.created_overlays'));
111
+ }
100
112
  showStep(t('installer.created_memories'));
101
113
  showStep(t('installer.installed_intelligence'));
102
114
  showStep(`${t('installer.configured_mcps')} ${selectedMCPs.join(', ')}`);
115
+ if (telemetryEnabled) {
116
+ showStep('Telemetry: enabled (anonymous, opt-in)');
117
+ }
103
118
 
104
119
  // Validation
105
120
  console.log();
@@ -133,11 +148,18 @@ export async function runWizard(targetDir, options = {}) {
133
148
  'codex-cli': '$chati',
134
149
  };
135
150
  const invokeCmd = invokeCmdMap[primaryIDE] || '/chati';
136
- showQuickStart(t('installer.quick_start_title'), [
151
+ const quickStartSteps = [
137
152
  `${t('installer.quick_start_1')} (${primaryIDEName})`,
138
153
  `Type: ${invokeCmd}`,
139
154
  t('installer.quick_start_3'),
140
- ]);
155
+ ];
156
+
157
+ // Add switch hint if multiple CLI providers configured
158
+ if (cliProviders.length > 1) {
159
+ quickStartSteps.push(t('installer.quick_start_switch_hint'));
160
+ }
161
+
162
+ showQuickStart(t('installer.quick_start_title'), quickStartSteps);
141
163
 
142
164
  return { success: true, config, validation };
143
165
  } catch (err) {
@@ -4,7 +4,7 @@ import { detectProjectType } from '../utils/detector.js';
4
4
  import { showSummary, showChecklist } from './feedback.js';
5
5
  import { brand, dim, success } from '../utils/colors.js';
6
6
  import { isProviderAvailable } from '../terminal/cli-registry.js';
7
- import { IDE_CONFIGS } from '../config/ide-configs.js';
7
+ import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
8
8
 
9
9
  /**
10
10
  * Step 1: Language Selection (always in English)
@@ -60,73 +60,212 @@ export async function stepProjectType(targetDir) {
60
60
  }
61
61
 
62
62
  /**
63
- * Step 3: LLM Provider Selection
63
+ * Provider display names for confirmation and selection prompts.
64
64
  */
65
- export async function stepLlmProvider() {
66
- const providers = [
67
- { name: 'claude', label: t('installer.llm_provider_claude') },
68
- { name: 'gemini', label: t('installer.llm_provider_gemini') },
69
- { name: 'codex', label: t('installer.llm_provider_codex') },
70
- ];
71
-
72
- // Check availability of each provider
73
- const availability = {};
74
- for (const prov of providers) {
65
+ const PROVIDER_DISPLAY_NAMES = {
66
+ claude: 'Claude (Anthropic)',
67
+ gemini: 'Gemini (Google)',
68
+ codex: 'Codex (OpenAI)',
69
+ };
70
+
71
+ /**
72
+ * Step 3a: Provider Selection (multi-select)
73
+ *
74
+ * Asks the user which AI providers they want to use (Claude, Gemini, Codex).
75
+ * Returns an array of provider names (e.g. ['claude', 'gemini']).
76
+ */
77
+ export async function stepProviderSelection() {
78
+ const cliIDEs = Object.entries(IDE_CONFIGS).filter(([, c]) => c.group === 'cli');
79
+ const options = [];
80
+
81
+ for (const [key, config] of cliIDEs) {
82
+ const provider = IDE_TO_PROVIDER[key];
83
+ let installed = false;
75
84
  try {
76
- availability[prov.name] = await isProviderAvailable(prov.name);
85
+ installed = provider ? await isProviderAvailable(provider) : false;
77
86
  } catch {
78
- availability[prov.name] = prov.name === 'claude'; // claude always available
87
+ installed = key === 'claude-code';
79
88
  }
89
+ const suffix = !installed ? ` ${dim(t('installer.llm_provider_not_installed'))}` : '';
90
+ const providerName = PROVIDER_DISPLAY_NAMES[provider] || config.name;
91
+ options.push({
92
+ value: provider,
93
+ label: `${providerName}${config.recommended ? ' (Recommended)' : ''}${suffix}`,
94
+ hint: config.description,
95
+ });
80
96
  }
81
97
 
82
- const options = providers.map(prov => {
83
- const installed = availability[prov.name];
84
- const suffix = installed ? '' : ` ${dim(t('installer.llm_provider_not_installed'))}`;
85
- return {
86
- value: prov.name,
87
- label: `${prov.label}${suffix}`,
88
- };
98
+ const selected = await p.multiselect({
99
+ message: t('installer.provider_selection_title'),
100
+ options,
101
+ required: true,
89
102
  });
90
103
 
91
- const provider = await p.select({
92
- message: t('installer.llm_provider_title'),
104
+ if (p.isCancel(selected)) {
105
+ p.cancel('Installation cancelled.');
106
+ process.exit(0);
107
+ }
108
+
109
+ return selected;
110
+ }
111
+
112
+ /**
113
+ * Step 3b: Editor Selection (multi-select, optional)
114
+ *
115
+ * Asks the user which editor IDEs should receive rules files.
116
+ * Returns an array of IDE keys (e.g. ['vscode', 'cursor']).
117
+ */
118
+ export async function stepEditorSelection() {
119
+ const editorIDEs = Object.entries(IDE_CONFIGS).filter(([, c]) => c.group === 'editor');
120
+
121
+ if (editorIDEs.length === 0) return [];
122
+
123
+ const options = editorIDEs.map(([key, config]) => ({
124
+ value: key,
125
+ label: config.name,
126
+ hint: config.description,
127
+ }));
128
+
129
+ const selected = await p.multiselect({
130
+ message: t('installer.editor_selection_title'),
93
131
  options,
94
- initialValue: 'claude',
132
+ required: false,
95
133
  });
96
134
 
97
- if (p.isCancel(provider)) {
135
+ if (p.isCancel(selected)) {
98
136
  p.cancel('Installation cancelled.');
99
137
  process.exit(0);
100
138
  }
101
139
 
102
- return provider;
140
+ return selected;
141
+ }
142
+
143
+ /**
144
+ * Step 3: IDE / CLI Selection (multi-select) — LEGACY
145
+ *
146
+ * Maintained for backward compatibility. Combines providers and editors in a single prompt.
147
+ * Users select which IDEs/CLIs they'll use; CLI providers are derived from selections.
148
+ */
149
+ export async function stepIDESelection() {
150
+ // Check availability of CLI-based IDEs
151
+ const cliIDEs = Object.entries(IDE_CONFIGS).filter(([, c]) => c.group === 'cli').map(([k]) => k);
152
+ const availability = {};
153
+ for (const ide of cliIDEs) {
154
+ const provider = IDE_TO_PROVIDER[ide];
155
+ try {
156
+ availability[ide] = provider ? await isProviderAvailable(provider) : false;
157
+ } catch {
158
+ availability[ide] = ide === 'claude-code';
159
+ }
160
+ }
161
+
162
+ // Build options with visual group separators
163
+ const options = [];
164
+
165
+ // CLI providers first (spawn terminals, enable multi-provider continuity)
166
+ options.push({ value: '_cli_header', label: dim('── CLI Providers (spawn agent terminals) ──'), hint: '', disabled: true });
167
+ for (const [key, config] of Object.entries(IDE_CONFIGS)) {
168
+ if (config.group !== 'cli') continue;
169
+ const installed = availability[key];
170
+ const suffix = !installed ? ` ${dim(t('installer.llm_provider_not_installed'))}` : '';
171
+ options.push({
172
+ value: key,
173
+ label: `${config.name}${config.recommended ? ' (Recommended)' : ''}${suffix}`,
174
+ hint: config.description,
175
+ });
176
+ }
177
+
178
+ // Editor IDEs (rules files only, no terminal spawning)
179
+ options.push({ value: '_editor_header', label: dim('── Editor IDEs (rules integration) ──'), hint: '', disabled: true });
180
+ for (const [key, config] of Object.entries(IDE_CONFIGS)) {
181
+ if (config.group !== 'editor') continue;
182
+ options.push({
183
+ value: key,
184
+ label: config.name,
185
+ hint: config.description,
186
+ });
187
+ }
188
+
189
+ const selectedIDEs = await p.multiselect({
190
+ message: t('installer.ide_selection_title'),
191
+ options,
192
+ required: true,
193
+ });
194
+
195
+ if (p.isCancel(selectedIDEs)) {
196
+ p.cancel('Installation cancelled.');
197
+ process.exit(0);
198
+ }
199
+
200
+ // Filter out separator headers
201
+ return selectedIDEs.filter(id => !id.startsWith('_'));
202
+ }
203
+
204
+ /**
205
+ * Step 3b: Primary CLI Provider Selection
206
+ *
207
+ * Only shown when the user selected multiple CLI-based IDEs.
208
+ * Determines which provider's adapted files go in chati.dev/ (main).
209
+ */
210
+ export async function stepPrimaryProvider(cliProviders) {
211
+ if (cliProviders.length <= 1) return cliProviders[0] || 'claude';
212
+
213
+ const primary = await p.select({
214
+ message: t('installer.primary_provider_title'),
215
+ options: cliProviders.map(prov => ({
216
+ value: prov,
217
+ label: PROVIDER_DISPLAY_NAMES[prov] || prov,
218
+ })),
219
+ });
220
+
221
+ if (p.isCancel(primary)) {
222
+ p.cancel('Installation cancelled.');
223
+ process.exit(0);
224
+ }
225
+
226
+ return primary;
103
227
  }
104
228
 
105
229
  /**
106
230
  * Step 4: Confirmation
107
231
  */
108
232
  export async function stepConfirmation(config) {
109
- const { projectName, projectType, language, llmProvider, selectedMCPs, selectedIDEs } = config;
233
+ const { projectName, projectType, language, llmProvider, selectedMCPs, selectedIDEs, allProviders } = config;
110
234
 
111
235
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
112
- const providerNames = { claude: 'Claude (Anthropic)', gemini: 'Gemini (Google)', codex: 'Codex (OpenAI)' };
113
- const providerDisplay = providerNames[llmProvider] || llmProvider;
114
- const ideNames = (selectedIDEs || ['claude-code'])
115
- .map(id => IDE_CONFIGS[id]?.name || id)
116
- .join(', ') + ' (auto-configured)';
236
+
237
+ // Show all providers with primary indicator
238
+ const providers = allProviders || [llmProvider || 'claude'];
239
+ const providersDisplay = providers.map(prov => {
240
+ const name = PROVIDER_DISPLAY_NAMES[prov] || prov;
241
+ return prov === llmProvider ? `${name} (Primary)` : name;
242
+ }).join(', ');
243
+
244
+ // Separate editors from CLI IDEs for display
245
+ const editorIDEs = (selectedIDEs || []).filter(id => IDE_CONFIGS[id]?.group === 'editor');
246
+ const editorNames = editorIDEs.length > 0
247
+ ? editorIDEs.map(id => IDE_CONFIGS[id]?.name || id).join(', ')
248
+ : 'None';
117
249
  const mcpNames = selectedMCPs.length > 0
118
250
  ? `${selectedMCPs.join(', ')} (auto-installed)`
119
251
  : 'None';
120
252
 
121
- console.log();
122
- console.log(brand(t('installer.confirmation_title') + ':'));
123
- showSummary({
253
+ const summaryData = {
124
254
  [t('installer.project_label')]: `${projectName} (${projectType === 'greenfield' ? 'Greenfield' : 'Brownfield'})`,
125
255
  [t('installer.language_label')]: langName,
126
- [t('installer.llm_provider_label')]: providerDisplay,
127
- [t('installer.ides_label')]: ideNames,
128
- [t('installer.mcps_label')]: mcpNames,
129
- });
256
+ [t('installer.providers_label')]: providersDisplay,
257
+ };
258
+
259
+ // Only show editor IDEs line if any were selected
260
+ if (editorIDEs.length > 0) {
261
+ summaryData[t('installer.ides_label')] = editorNames;
262
+ }
263
+
264
+ summaryData[t('installer.mcps_label')] = mcpNames;
265
+
266
+ console.log();
267
+ console.log(brand(t('installer.confirmation_title') + ':'));
268
+ showSummary(summaryData);
130
269
 
131
270
  console.log();
132
271
  console.log(` ${t('installer.will_install')}:`);
@@ -154,3 +293,25 @@ export async function stepConfirmation(config) {
154
293
 
155
294
  return true;
156
295
  }
296
+
297
+ /**
298
+ * Step 5: Telemetry Consent (opt-in)
299
+ *
300
+ * Asks the user if they want to share anonymous usage metrics.
301
+ * Returns true (opt-in) or false (opt-out).
302
+ */
303
+ export async function stepTelemetryConsent() {
304
+ p.note(
305
+ `${dim(t('installer.telemetry_description'))}\n\n${dim(t('installer.telemetry_privacy'))}`,
306
+ dim('Telemetry')
307
+ );
308
+
309
+ const consent = await p.confirm({
310
+ message: t('installer.telemetry_consent'),
311
+ initialValue: false,
312
+ });
313
+
314
+ if (p.isCancel(consent)) return false;
315
+
316
+ return consent;
317
+ }