chati-dev 3.2.4 → 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.
- package/LICENSE +96 -0
- package/README.md +2 -2
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +5 -3
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- 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
|
+
};
|
package/src/wizard/i18n.js
CHANGED
|
@@ -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}',
|
package/src/wizard/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
50
|
-
const
|
|
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
|
|
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
|
-
|
|
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) {
|
package/src/wizard/questions.js
CHANGED
|
@@ -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
|
-
*
|
|
63
|
+
* Provider display names for confirmation and selection prompts.
|
|
64
64
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
+
installed = provider ? await isProviderAvailable(provider) : false;
|
|
77
86
|
} catch {
|
|
78
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
132
|
+
required: false,
|
|
95
133
|
});
|
|
96
134
|
|
|
97
|
-
if (p.isCancel(
|
|
135
|
+
if (p.isCancel(selected)) {
|
|
98
136
|
p.cancel('Installation cancelled.');
|
|
99
137
|
process.exit(0);
|
|
100
138
|
}
|
|
101
139
|
|
|
102
|
-
return
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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.
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|