chati-dev 4.5.3 → 4.5.4

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.
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "4.5.3"
2
+ version: "4.5.4"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
4
  updated_at: "2026-06-11T00:00:00Z"
5
- installer_version: "4.5.3"
5
+ installer_version: "4.5.4"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: [claude-code]
@@ -1,7 +1,7 @@
1
1
  # Chati.dev System Context
2
2
 
3
3
  ## Framework
4
- - **Version**: 4.5.3
4
+ - **Version**: 4.5.4
5
5
  - **Agents**: Specialized discovery and planning agents feeding provider-neutral RAIL execution and a sealed release lane
6
6
  - **Constitution**: 25 Articles + Preamble
7
7
  - **Quality**: 5 pipeline gates + 3-tier verdicts + Fault Vector Protocol (Article XXII)
@@ -24,10 +24,13 @@ installer:
24
24
  ide_selection_title: "Select your IDEs / CLIs (multiple allowed):"
25
25
  installation_mode_title: "Select the installation mode"
26
26
  installation_mode_open: "Open"
27
- installation_mode_open_hint: "Public framework without mandatory company integrations"
27
+ installation_mode_open_hint: "Public framework"
28
28
  installation_mode_internal: "Internal"
29
- installation_mode_internal_hint: "Requires an authorized license and enables ClickUp tracking plus internal policies"
29
+ installation_mode_internal_hint: "Requires an authorized license"
30
30
  installation_mode_label: "Installation mode"
31
+ back: "Back"
32
+ back_hint: "Return to the previous step"
33
+ install: "Install"
31
34
  model_routing_label: "Model routing"
32
35
  model_routing_automatic: "Automatic per task across all enabled models"
33
36
  providers_label: "Providers"
@@ -24,10 +24,13 @@ installer:
24
24
  ide_selection_title: "Seleccione sus IDEs / CLIs (multiples permitidos):"
25
25
  installation_mode_title: "Seleccione el modo de instalacion"
26
26
  installation_mode_open: "Open"
27
- installation_mode_open_hint: "Framework publico sin integraciones corporativas obligatorias"
27
+ installation_mode_open_hint: "Framework publico"
28
28
  installation_mode_internal: "Internal"
29
- installation_mode_internal_hint: "Requiere una licencia autorizada y activa el tracking en ClickUp y las politicas internas"
29
+ installation_mode_internal_hint: "Requiere una licencia autorizada"
30
30
  installation_mode_label: "Modo de instalacion"
31
+ back: "Volver"
32
+ back_hint: "Regresar al paso anterior"
33
+ install: "Instalar"
31
34
  model_routing_label: "Enrutamiento de modelos"
32
35
  model_routing_automatic: "Automatico por tarea entre todos los modelos habilitados"
33
36
  providers_label: "Proveedores"
@@ -24,10 +24,13 @@ installer:
24
24
  ide_selection_title: "Selectionnez vos IDEs / CLIs (multiples autorises):"
25
25
  installation_mode_title: "Selectionnez le mode d'installation"
26
26
  installation_mode_open: "Open"
27
- installation_mode_open_hint: "Framework public sans integrations d'entreprise obligatoires"
27
+ installation_mode_open_hint: "Framework public"
28
28
  installation_mode_internal: "Internal"
29
- installation_mode_internal_hint: "Necessite une licence autorisee et active le suivi ClickUp et les politiques internes"
29
+ installation_mode_internal_hint: "Necessite une licence autorisee"
30
30
  installation_mode_label: "Mode d'installation"
31
+ back: "Retour"
32
+ back_hint: "Revenir a l'etape precedente"
33
+ install: "Installer"
31
34
  model_routing_label: "Routage des modeles"
32
35
  model_routing_automatic: "Automatique par tache entre tous les modeles actives"
33
36
  providers_label: "Fournisseurs"
@@ -24,10 +24,13 @@ installer:
24
24
  ide_selection_title: "Selecione suas IDEs / CLIs (multiplos permitidos):"
25
25
  installation_mode_title: "Selecione o modo de instalacao"
26
26
  installation_mode_open: "Open"
27
- installation_mode_open_hint: "Framework publico sem integracoes corporativas obrigatorias"
27
+ installation_mode_open_hint: "Framework publico"
28
28
  installation_mode_internal: "Internal"
29
- installation_mode_internal_hint: "Exige uma licenca autorizada e ativa o tracking no ClickUp e as politicas internas"
29
+ installation_mode_internal_hint: "Exige uma licenca autorizada"
30
30
  installation_mode_label: "Modo de instalacao"
31
+ back: "Voltar"
32
+ back_hint: "Retornar a etapa anterior"
33
+ install: "Instalar"
31
34
  model_routing_label: "Roteamento de modelos"
32
35
  model_routing_automatic: "Automatico por task entre todos os modelos habilitados"
33
36
  providers_label: "Provedores"
@@ -1,5 +1,5 @@
1
1
  {
2
- "createdAt": "2026-08-27T07:35:51.100Z",
2
+ "createdAt": "2026-08-27T07:55:29.821Z",
3
3
  "files": {
4
4
  "agents/build/dev.md": {
5
5
  "hash": "5c8da2eee3357bbe84a6c468c76fe60a0c77357575503c3797aa94128282c565",
@@ -97,7 +97,7 @@
97
97
  "type": "md"
98
98
  },
99
99
  "config.yaml": {
100
- "hash": "49815a639e15886001dbcd8fcf778212e29b28dc14e960cbce1e6bef9cf60c0e",
100
+ "hash": "c031e5565adc3a1dcbfd2534e20b486fd9563158a36d10e2b1f2f73874eb76f4",
101
101
  "size": 2841,
102
102
  "type": "yaml"
103
103
  },
@@ -122,7 +122,7 @@
122
122
  "type": "md"
123
123
  },
124
124
  "context/root.md": {
125
- "hash": "51499195d5281ee92d6414f65c7847fda86cf4a4faea006403f5b1d5b6a575ad",
125
+ "hash": "046ff16a4973c4d73d186849bd734914d6b90a9864d5bd41f435991b7004c8b2",
126
126
  "size": 1217,
127
127
  "type": "md"
128
128
  },
@@ -377,23 +377,23 @@
377
377
  "type": "js"
378
378
  },
379
379
  "i18n/en.yaml": {
380
- "hash": "5eb0e9dfa7baec166fc12ca8a1c0e9623c9a023c51c09455b80a6d8df1e0205f",
381
- "size": 4941,
380
+ "hash": "b5e9bf16c930a61543b17088a7267caea0c94eb32bfe0ae2e874ba15f5cdb4aa",
381
+ "size": 4929,
382
382
  "type": "yaml"
383
383
  },
384
384
  "i18n/es.yaml": {
385
- "hash": "1ed8c732c13c23e3fc2d5578526c1285a2a1ffc1d79b85d8e12365c3bcbbd882",
386
- "size": 5225,
385
+ "hash": "5407e4c6adba697da8bf2e2dbdf39f1fd32935326166852d5b488487148320f1",
386
+ "size": 5204,
387
387
  "type": "yaml"
388
388
  },
389
389
  "i18n/fr.yaml": {
390
- "hash": "ae88b914ea39d6613879aae3a3fcdf92a771d3df0f207def6cbb2cc76455cfe9",
391
- "size": 5290,
390
+ "hash": "2e3b50c56ee5f05f81c4e48fd048c679baae2280e3a9c03c589f8f82989eded4",
391
+ "size": 5276,
392
392
  "type": "yaml"
393
393
  },
394
394
  "i18n/pt.yaml": {
395
- "hash": "9e766ca8c7eba6362816bdc9d1f6162d615f1b5ca1a9b2f75071a53b7ab9f6b3",
396
- "size": 5163,
395
+ "hash": "da65aff46c8f16b1c6fd6be10b89b566253eec71fea0a42094878dcc9e4671fa",
396
+ "size": 5147,
397
397
  "type": "yaml"
398
398
  },
399
399
  "intelligence/confidence.yaml": {
@@ -1347,5 +1347,5 @@
1347
1347
  "type": "yaml"
1348
1348
  }
1349
1349
  },
1350
- "version": "4.5.3"
1350
+ "version": "4.5.4"
1351
1351
  }
@@ -1 +1 @@
1
- 6nwMwMBZREoj26RzRPuaKZahTh6Vxi/THZcqOmJpLZ+F07b60aSvXLp6vVyRDPnCw6XizfMHx81hdg0iQzcVDw==
1
+ 8t69CRtft7FLT0qSIiCVC7zuHbWVEVXvYtQTxm9B6/Ln3+J5UC0rlzl3mI69qD2P8tZPUNyCbdaWfLbwSyVaBw==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.5.3",
3
+ "version": "4.5.4",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -141,6 +141,26 @@ export function hasLicenseEntitlement(status, entitlement) {
141
141
  return status?.status === 'VALID' && normalizeEntitlements(status.entitlements).includes(entitlement);
142
142
  }
143
143
 
144
+ /**
145
+ * Verifies that a saved key is registered for the current machine. A copied
146
+ * global license file may contain a valid key associated with another host.
147
+ */
148
+ export async function ensureCurrentMachineLicense({
149
+ key = getLicenseKey(),
150
+ validate = validateLicense,
151
+ activate = activateLicense,
152
+ } = {}) {
153
+ if (!key) return { status: 'MISSING' };
154
+ const status = await validate(null);
155
+ if (status.status === 'VALID' || status.reason !== 'Machine not authorized') return status;
156
+ const activated = await activate(null, key);
157
+ return {
158
+ ...activated,
159
+ status: 'VALID',
160
+ entitlements: normalizeEntitlements(activated.entitlements),
161
+ };
162
+ }
163
+
144
164
  /**
145
165
  * Refreshes the server-authoritative license status and requires one
146
166
  * entitlement before an internal installation can proceed.
@@ -148,7 +168,10 @@ export function hasLicenseEntitlement(status, entitlement) {
148
168
  export async function requireLicenseEntitlement(entitlement, { validate = validateLicense } = {}) {
149
169
  const status = await validate(null);
150
170
  if (!hasLicenseEntitlement(status, entitlement)) {
151
- throw Object.assign(new Error(`The active license is not authorized for ${entitlement} mode.`), {
171
+ const message = status?.status !== 'VALID' && status?.reason
172
+ ? `License validation failed: ${status.reason}.`
173
+ : `The active license is not authorized for ${entitlement} mode.`;
174
+ throw Object.assign(new Error(message), {
152
175
  code: 'LICENSE_ENTITLEMENT_REQUIRED',
153
176
  entitlement,
154
177
  });
@@ -32,10 +32,13 @@ const FALLBACK_EN = {
32
32
  editor_selection_title: 'Which editor IDEs should get rules files? (optional)',
33
33
  installation_mode_title: 'Select the installation mode',
34
34
  installation_mode_open: 'Open',
35
- installation_mode_open_hint: 'Public framework without mandatory company integrations',
35
+ installation_mode_open_hint: 'Public framework',
36
36
  installation_mode_internal: 'Internal',
37
- installation_mode_internal_hint: 'Requires an authorized license and enables ClickUp tracking plus internal policies',
37
+ installation_mode_internal_hint: 'Requires an authorized license',
38
38
  installation_mode_label: 'Installation mode',
39
+ back: 'Back',
40
+ back_hint: 'Return to the previous step',
41
+ install: 'Install',
39
42
  model_routing_label: 'Model routing',
40
43
  model_routing_automatic: 'Automatic per task across all enabled models',
41
44
  providers_label: 'Providers',
@@ -3,7 +3,7 @@ import { existsSync, 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, stepProviderSelection, stepEditorSelection, stepModelSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
6
+ import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepEditorSelection, stepModelSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { INSTALLATION_ARTIFACT_PATH, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
@@ -14,7 +14,7 @@ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from
14
14
  import { sendEvents } from '../telemetry/sender.js';
15
15
  import { getTelemetryConfig } from '../telemetry/config.js';
16
16
  import { t } from './i18n.js';
17
- import { getLicenseKey, activateLicense, requireLicenseEntitlement } from '../license/client.js';
17
+ import { getLicenseKey, activateLicense, ensureCurrentMachineLicense, requireLicenseEntitlement } from '../license/client.js';
18
18
  import { DEFAULT_MCPS } from '../config/mcp-configs.js';
19
19
  import { IDE_CONFIGS } from '../config/ide-configs.js';
20
20
 
@@ -56,81 +56,159 @@ export async function runWizard(targetDir, options = {}) {
56
56
  p.log.success(`System check:\n${preflightFmt}`);
57
57
 
58
58
  // Step 2: Language Selection
59
- const language = options.language || await stepLanguage();
59
+ let language = options.language || await stepLanguage();
60
60
 
61
61
  // Step 2: Terms of Use (must accept to continue)
62
62
  if (options.telemetry === undefined) {
63
63
  await stepTermsOfUse();
64
64
  }
65
65
 
66
- // Step 3: Project Type
67
- const projectType = options.projectType || await stepProjectType(targetDir);
68
-
69
- // Step 3a: Provider Selection (which AI providers to use)
70
- const selectedProviders = options.providers || await stepProviderSelection();
71
-
72
- // Step 3b: Enable the full signed lineup for every selected provider. Runtime
73
- // routing selects the right model per task instead of pinning one at install.
74
66
  if (options.capabilityCatalog) {
75
67
  throw Object.assign(new Error('unsigned capabilityCatalog injection is forbidden; provide signedCapabilityCatalogEnvelope'), { code: 'UNSIGNED_CATALOG_FORBIDDEN' });
76
68
  }
77
- const catalogResolution = options.signedCapabilityCatalogEnvelope
78
- ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
79
- : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
80
- const modelSelections = options.modelSelections || await stepModelSelection(selectedProviders, catalogResolution.catalog);
81
-
82
- // Step 3c: Internal mode is a server-authorized entitlement. It enables the
83
- // internal policy bundle, including mandatory ClickUp tracking.
84
- const installationMode = options.installationMode || await stepInstallationMode();
85
- if (installationMode === 'internal') {
86
- await requireLicenseEntitlement('internal', { validate: options.licenseValidator });
87
- }
88
69
 
89
- // Step 3e: Editor Selection (which editor IDEs get rules files)
90
- const selectedEditors = options.editors || await stepEditorSelection();
91
-
92
- // Combine providers + editors into selectedIDEs for backward compatibility
93
- const providerToIDE = { claude: 'claude-code', gemini: 'gemini-cli', codex: 'codex-cli', grok: 'grok-cli' };
94
- const selectedIDEs = options.ides || [
95
- ...selectedProviders.map(p => providerToIDE[p]).filter(Boolean),
96
- ...selectedEditors,
97
- ];
98
-
99
- // CLI providers derived from selected providers
100
- const cliProviders = selectedProviders;
101
-
102
- const selectedMCPs = options.mcps || DEFAULT_MCPS;
103
-
104
- // Step 4: Confirmation
105
- const projectName = basename(targetDir);
106
- const config = {
107
- projectName,
108
- projectType,
109
- language,
110
- allProviders: cliProviders,
111
- selectedIDEs,
112
- selectedMCPs,
113
- installationMode,
114
- modelSelections,
115
- targetDir,
116
- version: VERSION,
70
+ let projectType;
71
+ let selectedProviders;
72
+ let catalogResolution;
73
+ let modelSelections;
74
+ let installationMode;
75
+ let selectedEditors;
76
+ let config;
77
+ let v2InstallationInput;
78
+ let stage = 'project';
79
+
80
+ const stageOrder = ['language', 'project', 'providers', 'mode', 'editors', 'confirm'];
81
+ const optionKey = {
82
+ language: 'language',
83
+ project: 'projectType',
84
+ providers: 'providers',
85
+ mode: 'installationMode',
86
+ editors: 'editors',
117
87
  };
118
- const v2InstallationInput = buildWizardV2InstallationInput({
119
- projectName,
120
- selectedProviders: cliProviders,
121
- modelSelections,
122
- capabilityCatalog: catalogResolution.catalog,
123
- installationMode,
124
- });
125
- // Validate the complete v2 material and refuse a repeated init before the
126
- // legacy installer writes anything. Reconfiguration uses its own backup and
127
- // atomic replacement path below.
128
- dryRunV2(v2InstallationInput, { projectDir: targetDir });
129
- if (existsSync(join(targetDir, INSTALLATION_ARTIFACT_PATH)) && options.reconfigure !== true) {
130
- throw Object.assign(new Error('a v2 installation already exists; use --reconfigure to replace it safely'), { code: 'INSTALLATION_ALREADY_EXISTS' });
88
+ const previousInteractiveStage = (current) => {
89
+ for (let index = stageOrder.indexOf(current) - 1; index >= 0; index -= 1) {
90
+ const candidate = stageOrder[index];
91
+ if (candidate === 'language' ? options.language === undefined : options[optionKey[candidate]] === undefined) {
92
+ return candidate;
93
+ }
94
+ }
95
+ return null;
96
+ };
97
+
98
+ while (stage !== 'complete') {
99
+ if (stage === 'language') {
100
+ language = await stepLanguage();
101
+ stage = 'project';
102
+ continue;
103
+ }
104
+
105
+ if (stage === 'project') {
106
+ const value = options.projectType !== undefined
107
+ ? options.projectType
108
+ : await stepProjectType(targetDir, { allowBack: previousInteractiveStage('project') !== null });
109
+ if (value === WIZARD_BACK) {
110
+ stage = previousInteractiveStage('project');
111
+ continue;
112
+ }
113
+ projectType = value;
114
+ stage = 'providers';
115
+ continue;
116
+ }
117
+
118
+ if (stage === 'providers') {
119
+ const value = options.providers !== undefined
120
+ ? options.providers
121
+ : await stepProviderSelection({ allowBack: previousInteractiveStage('providers') !== null });
122
+ if (value === WIZARD_BACK) {
123
+ stage = previousInteractiveStage('providers');
124
+ continue;
125
+ }
126
+ selectedProviders = value;
127
+ catalogResolution ??= options.signedCapabilityCatalogEnvelope
128
+ ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
129
+ : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
130
+ modelSelections = options.modelSelections !== undefined
131
+ ? options.modelSelections
132
+ : await stepModelSelection(selectedProviders, catalogResolution.catalog);
133
+ stage = 'mode';
134
+ continue;
135
+ }
136
+
137
+ if (stage === 'mode') {
138
+ const value = options.installationMode !== undefined
139
+ ? options.installationMode
140
+ : await stepInstallationMode({ allowBack: previousInteractiveStage('mode') !== null });
141
+ if (value === WIZARD_BACK) {
142
+ stage = previousInteractiveStage('mode');
143
+ continue;
144
+ }
145
+ installationMode = value;
146
+ if (installationMode === 'internal') {
147
+ try {
148
+ await requireLicenseEntitlement('internal', { validate: options.licenseValidator });
149
+ } catch (error) {
150
+ if (options.installationMode !== undefined) throw error;
151
+ p.log.error(error.message);
152
+ continue;
153
+ }
154
+ }
155
+ stage = 'editors';
156
+ continue;
157
+ }
158
+
159
+ if (stage === 'editors') {
160
+ const value = options.editors !== undefined
161
+ ? options.editors
162
+ : await stepEditorSelection({ allowBack: previousInteractiveStage('editors') !== null });
163
+ if (value === WIZARD_BACK) {
164
+ stage = previousInteractiveStage('editors');
165
+ continue;
166
+ }
167
+ selectedEditors = value;
168
+ stage = 'confirm';
169
+ continue;
170
+ }
171
+
172
+ const providerToIDE = { claude: 'claude-code', gemini: 'gemini-cli', codex: 'codex-cli', grok: 'grok-cli' };
173
+ const selectedIDEs = options.ides || [
174
+ ...selectedProviders.map(provider => providerToIDE[provider]).filter(Boolean),
175
+ ...selectedEditors,
176
+ ];
177
+ const cliProviders = selectedProviders;
178
+ const selectedMCPs = options.mcps || DEFAULT_MCPS;
179
+ const projectName = basename(targetDir);
180
+ config = {
181
+ projectName,
182
+ projectType,
183
+ language,
184
+ allProviders: cliProviders,
185
+ selectedIDEs,
186
+ selectedMCPs,
187
+ installationMode,
188
+ modelSelections,
189
+ targetDir,
190
+ version: VERSION,
191
+ };
192
+ v2InstallationInput = buildWizardV2InstallationInput({
193
+ projectName,
194
+ selectedProviders: cliProviders,
195
+ modelSelections,
196
+ capabilityCatalog: catalogResolution.catalog,
197
+ installationMode,
198
+ });
199
+ dryRunV2(v2InstallationInput, { projectDir: targetDir });
200
+ if (existsSync(join(targetDir, INSTALLATION_ARTIFACT_PATH)) && options.reconfigure !== true) {
201
+ throw Object.assign(new Error('a v2 installation already exists; use --reconfigure to replace it safely'), { code: 'INSTALLATION_ALREADY_EXISTS' });
202
+ }
203
+ const confirmation = await stepConfirmation(config, { allowBack: previousInteractiveStage('confirm') !== null });
204
+ if (confirmation === WIZARD_BACK) {
205
+ stage = previousInteractiveStage('confirm');
206
+ continue;
207
+ }
208
+ stage = 'complete';
131
209
  }
132
210
 
133
- await stepConfirmation(config);
211
+ const { selectedIDEs, selectedMCPs, allProviders: cliProviders } = config;
134
212
 
135
213
  // Telemetry: enabled by default (opt-out model via ToS)
136
214
  config.telemetryEnabled = options.telemetry !== undefined ? options.telemetry : true;
@@ -250,7 +328,15 @@ export async function runWizard(targetDir, options = {}) {
250
328
 
251
329
  async function runLicenseActivationStep() {
252
330
  const existingKey = getLicenseKey();
253
- if (existingKey) return; // already activated, proceed silently
331
+ if (existingKey) {
332
+ try {
333
+ const status = await ensureCurrentMachineLicense({ key: existingKey });
334
+ if (status.status === 'VALID') return;
335
+ p.log.warn(`Saved license is not valid on this machine: ${status.reason || status.status}`);
336
+ } catch (error) {
337
+ p.log.warn(`Could not verify the saved license: ${error.message}`);
338
+ }
339
+ }
254
340
 
255
341
  console.log('\n ┌─ License Activation ─────────────────────────────────────────┐');
256
342
  console.log(' │ Your license key works across ALL projects on this machine. │');
@@ -7,6 +7,12 @@ import { isProviderAvailable } from '../terminal/cli-registry.js';
7
7
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
8
8
  import { supportedV2WizardProviders, wizardModelSuggestions } from '../installer-v2/wizard-installation.js';
9
9
 
10
+ export const WIZARD_BACK = '__chati_wizard_back__';
11
+
12
+ function backOption() {
13
+ return { value: WIZARD_BACK, label: `← ${t('installer.back')}`, hint: t('installer.back_hint') };
14
+ }
15
+
10
16
  /**
11
17
  * Step 1: Language Selection (always in English)
12
18
  */
@@ -33,7 +39,7 @@ export async function stepLanguage() {
33
39
  /**
34
40
  * Step 2: Project Type
35
41
  */
36
- export async function stepProjectType(targetDir) {
42
+ export async function stepProjectType(targetDir, { allowBack = false } = {}) {
37
43
  const detection = detectProjectType(targetDir);
38
44
 
39
45
  if (detection.suggestion === 'brownfield' && detection.confidence !== 'low') {
@@ -48,6 +54,7 @@ export async function stepProjectType(targetDir) {
48
54
  options: [
49
55
  { value: 'greenfield', label: t('installer.greenfield') },
50
56
  { value: 'brownfield', label: t('installer.brownfield') },
57
+ ...(allowBack ? [backOption()] : []),
51
58
  ],
52
59
  initialValue: detection.suggestion,
53
60
  });
@@ -76,7 +83,7 @@ const PROVIDER_DISPLAY_NAMES = {
76
83
  * Asks the user which AI providers they want to use (Claude, Gemini, Codex).
77
84
  * Returns an array of provider names (e.g. ['claude', 'gemini']).
78
85
  */
79
- export async function stepProviderSelection() {
86
+ export async function stepProviderSelection({ allowBack = false } = {}) {
80
87
  const cliIDEs = Object.entries(IDE_CONFIGS).filter(([key, c]) => c.group === 'cli' && supportedV2WizardProviders.includes(IDE_TO_PROVIDER[key]));
81
88
  const options = [];
82
89
 
@@ -100,7 +107,7 @@ export async function stepProviderSelection() {
100
107
 
101
108
  const selected = await p.multiselect({
102
109
  message: `${t('installer.provider_selection_title')} ${dim('(space to select, enter to confirm)')}`,
103
- options,
110
+ options: allowBack ? [...options, backOption()] : options,
104
111
  required: true,
105
112
  });
106
113
 
@@ -109,19 +116,20 @@ export async function stepProviderSelection() {
109
116
  process.exit(0);
110
117
  }
111
118
 
112
- return selected;
119
+ return selected.includes(WIZARD_BACK) ? WIZARD_BACK : selected;
113
120
  }
114
121
 
115
122
  /**
116
123
  * Selects the operational policy profile. Focus AI internal projects require
117
124
  * ClickUp tracking; standard projects do not gain that external dependency.
118
125
  */
119
- export async function stepInstallationMode() {
126
+ export async function stepInstallationMode({ allowBack = false } = {}) {
120
127
  const mode = await p.select({
121
128
  message: t('installer.installation_mode_title'),
122
129
  options: [
123
130
  { value: 'open', label: t('installer.installation_mode_open'), hint: t('installer.installation_mode_open_hint') },
124
131
  { value: 'internal', label: t('installer.installation_mode_internal'), hint: t('installer.installation_mode_internal_hint') },
132
+ ...(allowBack ? [backOption()] : []),
125
133
  ],
126
134
  initialValue: 'open',
127
135
  });
@@ -142,7 +150,7 @@ export const stepExecutionProfile = stepInstallationMode;
142
150
  * Asks the user which editor IDEs should receive rules files.
143
151
  * Returns an array of IDE keys (e.g. ['vscode', 'cursor']).
144
152
  */
145
- export async function stepEditorSelection() {
153
+ export async function stepEditorSelection({ allowBack = false } = {}) {
146
154
  const editorIDEs = Object.entries(IDE_CONFIGS).filter(([, c]) => c.group === 'editor');
147
155
 
148
156
  if (editorIDEs.length === 0) return [];
@@ -155,7 +163,7 @@ export async function stepEditorSelection() {
155
163
 
156
164
  const selected = await p.multiselect({
157
165
  message: `${t('installer.editor_selection_title')} ${dim('(space to select, enter to confirm)')}`,
158
- options,
166
+ options: allowBack ? [...options, backOption()] : options,
159
167
  required: false,
160
168
  });
161
169
 
@@ -164,7 +172,7 @@ export async function stepEditorSelection() {
164
172
  process.exit(0);
165
173
  }
166
174
 
167
- return selected;
175
+ return selected.includes(WIZARD_BACK) ? WIZARD_BACK : selected;
168
176
  }
169
177
 
170
178
  /**
@@ -252,7 +260,7 @@ export async function stepModelSelection(providers, capabilityCatalog) {
252
260
  /**
253
261
  * Step 4: Confirmation
254
262
  */
255
- export async function stepConfirmation(config) {
263
+ export async function stepConfirmation(config, { allowBack = false } = {}) {
256
264
  const { projectName, projectType, language, selectedMCPs, selectedIDEs, allProviders, installationMode } = config;
257
265
 
258
266
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
@@ -305,10 +313,21 @@ export async function stepConfirmation(config) {
305
313
  console.log();
306
314
  console.log(` ${dim('Target:')} ${config.targetDir}`);
307
315
 
308
- const proceed = await p.confirm({
309
- message: t('installer.proceed'),
310
- initialValue: true,
311
- });
316
+ const proceed = allowBack
317
+ ? await p.select({
318
+ message: t('installer.proceed'),
319
+ options: [
320
+ { value: true, label: t('installer.install') },
321
+ backOption(),
322
+ ],
323
+ initialValue: true,
324
+ })
325
+ : await p.confirm({
326
+ message: t('installer.proceed'),
327
+ initialValue: true,
328
+ });
329
+
330
+ if (proceed === WIZARD_BACK) return WIZARD_BACK;
312
331
 
313
332
  if (p.isCancel(proceed) || !proceed) {
314
333
  p.cancel('Installation cancelled.');