clavix 3.2.2 → 3.3.1

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.
@@ -12,17 +12,19 @@ export default class Config extends Command {
12
12
  };
13
13
  run(): Promise<void>;
14
14
  private showInteractiveMenu;
15
+ private manageProviders;
16
+ private addProviders;
17
+ private removeProviders;
18
+ private replaceProviders;
15
19
  private getConfig;
16
20
  private setConfig;
17
21
  private editConfig;
18
22
  private resetConfig;
19
- private changeAgent;
20
23
  private editPreferences;
21
24
  private loadConfig;
22
25
  private saveConfig;
23
26
  private displayConfig;
24
27
  private getNestedValue;
25
28
  private setNestedValue;
26
- private getDefaultConfig;
27
29
  }
28
30
  //# sourceMappingURL=config.d.ts.map
@@ -4,13 +4,14 @@ import fs from 'fs-extra';
4
4
  import * as path from 'path';
5
5
  import inquirer from 'inquirer';
6
6
  import { AgentManager } from '../../core/agent-manager.js';
7
+ import { DEFAULT_CONFIG, isLegacyConfig, migrateConfig } from '../../types/config.js';
8
+ import { loadCommandTemplates } from '../../utils/template-loader.js';
7
9
  export default class Config extends Command {
8
10
  static description = 'Manage Clavix configuration';
9
11
  static examples = [
10
12
  '<%= config.bin %> <%= command.id %>',
11
- '<%= config.bin %> <%= command.id %> get agent',
12
- '<%= config.bin %> <%= command.id %> set agent cursor',
13
- '<%= config.bin %> <%= command.id %> edit',
13
+ '<%= config.bin %> <%= command.id %> get providers',
14
+ '<%= config.bin %> <%= command.id %> set preferences.verboseLogging true',
14
15
  ];
15
16
  static args = {
16
17
  action: Args.string({
@@ -71,39 +72,246 @@ export default class Config extends Command {
71
72
  }
72
73
  }
73
74
  async showInteractiveMenu(configPath) {
74
- const config = this.loadConfig(configPath);
75
- this.log(chalk.bold.cyan('⚙️ Clavix Configuration\n'));
76
- this.displayConfig(config);
77
- const { action } = await inquirer.prompt([
75
+ let continueMenu = true;
76
+ while (continueMenu) {
77
+ const config = this.loadConfig(configPath);
78
+ this.log(chalk.bold.cyan('\n⚙️ Clavix Configuration\n'));
79
+ this.displayConfig(config);
80
+ const { action } = await inquirer.prompt([
81
+ {
82
+ type: 'list',
83
+ name: 'action',
84
+ message: 'What would you like to do?',
85
+ choices: [
86
+ { name: 'View current configuration', value: 'view' },
87
+ { name: 'Manage providers (add/remove)', value: 'providers' },
88
+ { name: 'Edit preferences', value: 'edit-preferences' },
89
+ { name: 'Reset to defaults', value: 'reset' },
90
+ { name: 'Exit', value: 'exit' },
91
+ ],
92
+ },
93
+ ]);
94
+ switch (action) {
95
+ case 'view':
96
+ // Already displayed above
97
+ break;
98
+ case 'providers':
99
+ await this.manageProviders(config, configPath);
100
+ break;
101
+ case 'edit-preferences':
102
+ await this.editPreferences(configPath, config);
103
+ break;
104
+ case 'reset':
105
+ await this.resetConfig(configPath);
106
+ break;
107
+ case 'exit':
108
+ continueMenu = false;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ async manageProviders(config, configPath) {
114
+ const agentManager = new AgentManager();
115
+ while (true) {
116
+ // Show current providers
117
+ this.log(chalk.cyan('\n📦 Current Providers:'));
118
+ if (config.providers.length === 0) {
119
+ this.log(chalk.gray(' (none configured)'));
120
+ }
121
+ else {
122
+ for (const providerName of config.providers) {
123
+ const adapter = agentManager.getAdapter(providerName);
124
+ const displayName = adapter?.displayName || providerName;
125
+ this.log(chalk.gray(` • ${displayName}`));
126
+ }
127
+ }
128
+ // Submenu
129
+ const { action } = await inquirer.prompt([
130
+ {
131
+ type: 'list',
132
+ name: 'action',
133
+ message: 'What would you like to do?',
134
+ choices: [
135
+ { name: 'Add provider', value: 'add' },
136
+ { name: 'Remove provider', value: 'remove' },
137
+ { name: 'Replace all providers', value: 'replace' },
138
+ { name: 'Back to main menu', value: 'back' },
139
+ ],
140
+ },
141
+ ]);
142
+ if (action === 'back')
143
+ break;
144
+ switch (action) {
145
+ case 'add':
146
+ await this.addProviders(config, configPath);
147
+ break;
148
+ case 'remove':
149
+ await this.removeProviders(config, configPath);
150
+ break;
151
+ case 'replace':
152
+ await this.replaceProviders(config, configPath);
153
+ break;
154
+ }
155
+ // Reload config after modifications
156
+ config = this.loadConfig(configPath);
157
+ }
158
+ }
159
+ async addProviders(config, configPath) {
160
+ const agentManager = new AgentManager();
161
+ const allProviders = agentManager.getAdapters();
162
+ // Show only non-selected providers
163
+ const availableToAdd = allProviders.filter((a) => !config.providers.includes(a.name));
164
+ if (availableToAdd.length === 0) {
165
+ this.log(chalk.yellow('\n✓ All providers already added!'));
166
+ return;
167
+ }
168
+ // Multi-select checkbox
169
+ const { newProviders } = await inquirer.prompt([
78
170
  {
79
- type: 'list',
80
- name: 'action',
81
- message: 'What would you like to do?',
82
- choices: [
83
- { name: 'View current configuration', value: 'view' },
84
- { name: 'Change agent', value: 'change-agent' },
85
- { name: 'Edit preferences', value: 'edit-preferences' },
86
- { name: 'Reset to defaults', value: 'reset' },
87
- { name: 'Exit', value: 'exit' },
88
- ],
171
+ type: 'checkbox',
172
+ name: 'newProviders',
173
+ message: 'Select providers to add:',
174
+ choices: availableToAdd.map((adapter) => ({
175
+ name: `${adapter.displayName} (${adapter.directory})`,
176
+ value: adapter.name,
177
+ })),
89
178
  },
90
179
  ]);
91
- switch (action) {
92
- case 'view':
93
- // Already displayed above
94
- break;
95
- case 'change-agent':
96
- await this.changeAgent(configPath, config);
97
- break;
98
- case 'edit-preferences':
99
- await this.editPreferences(configPath, config);
100
- break;
101
- case 'reset':
102
- await this.resetConfig(configPath);
103
- break;
104
- case 'exit':
105
- return;
180
+ if (newProviders.length === 0) {
181
+ this.log(chalk.gray('No providers selected'));
182
+ return;
106
183
  }
184
+ // Add to config
185
+ config.providers.push(...newProviders);
186
+ this.saveConfig(configPath, config);
187
+ this.log(chalk.gray('\n🔧 Generating commands for new providers...'));
188
+ // Generate commands for new providers
189
+ for (const providerName of newProviders) {
190
+ const adapter = agentManager.getAdapter(providerName);
191
+ if (!adapter)
192
+ continue;
193
+ const templates = await loadCommandTemplates(adapter);
194
+ await adapter.generateCommands(templates);
195
+ this.log(chalk.green(` ✓ Generated ${templates.length} command(s) for ${adapter.displayName}`));
196
+ }
197
+ this.log(chalk.green('\n✅ Providers added successfully!'));
198
+ }
199
+ async removeProviders(config, configPath) {
200
+ if (config.providers.length === 0) {
201
+ this.log(chalk.yellow('\n⚠ No providers configured!'));
202
+ return;
203
+ }
204
+ const agentManager = new AgentManager();
205
+ // Multi-select from current providers
206
+ const { providersToRemove } = await inquirer.prompt([
207
+ {
208
+ type: 'checkbox',
209
+ name: 'providersToRemove',
210
+ message: 'Select providers to remove:',
211
+ choices: config.providers.map((name) => {
212
+ const adapter = agentManager.getAdapter(name);
213
+ return {
214
+ name: `${adapter?.displayName || name} (${adapter?.directory || 'unknown'})`,
215
+ value: name,
216
+ };
217
+ }),
218
+ validate: (answer) => {
219
+ if (answer.length === config.providers.length) {
220
+ return 'You must keep at least one provider. Use "Reset to defaults" to reconfigure completely.';
221
+ }
222
+ return true;
223
+ },
224
+ },
225
+ ]);
226
+ if (providersToRemove.length === 0) {
227
+ this.log(chalk.gray('No providers selected'));
228
+ return;
229
+ }
230
+ // Confirm cleanup
231
+ const { cleanup } = await inquirer.prompt([
232
+ {
233
+ type: 'confirm',
234
+ name: 'cleanup',
235
+ message: 'Remove command files for these providers?',
236
+ default: true,
237
+ },
238
+ ]);
239
+ // Remove from config
240
+ config.providers = config.providers.filter((p) => !providersToRemove.includes(p));
241
+ this.saveConfig(configPath, config);
242
+ // Clean up command files
243
+ if (cleanup) {
244
+ this.log(chalk.gray('\n🗑️ Cleaning up command files...'));
245
+ for (const providerName of providersToRemove) {
246
+ const adapter = agentManager.getAdapter(providerName);
247
+ if (adapter) {
248
+ const removed = await adapter.removeAllCommands();
249
+ this.log(chalk.gray(` ✓ Removed ${removed} command(s) from ${adapter.displayName}`));
250
+ }
251
+ }
252
+ }
253
+ this.log(chalk.green('\n✅ Providers removed successfully!'));
254
+ }
255
+ async replaceProviders(config, configPath) {
256
+ const agentManager = new AgentManager();
257
+ // Use shared provider selector
258
+ const { selectProviders } = await import('../../utils/provider-selector.js');
259
+ const newProviders = await selectProviders(agentManager, config.providers);
260
+ if (newProviders.length === 0) {
261
+ this.log(chalk.gray('No providers selected'));
262
+ return;
263
+ }
264
+ // Find deselected providers
265
+ const deselected = config.providers.filter((p) => !newProviders.includes(p));
266
+ // Handle cleanup if providers were deselected
267
+ if (deselected.length > 0) {
268
+ this.log(chalk.yellow('\n⚠ Previously configured but not selected:'));
269
+ for (const providerName of deselected) {
270
+ const adapter = agentManager.getAdapter(providerName);
271
+ const displayName = adapter?.displayName || providerName;
272
+ const directory = adapter?.directory || 'unknown';
273
+ this.log(chalk.gray(` • ${displayName} (${directory})`));
274
+ }
275
+ const { cleanupAction } = await inquirer.prompt([
276
+ {
277
+ type: 'list',
278
+ name: 'cleanupAction',
279
+ message: 'What would you like to do with these providers?',
280
+ choices: [
281
+ { name: 'Clean up (remove all command files)', value: 'cleanup' },
282
+ { name: 'Skip (leave as-is)', value: 'skip' },
283
+ ],
284
+ },
285
+ ]);
286
+ if (cleanupAction === 'cleanup') {
287
+ this.log(chalk.gray('\n🗑️ Cleaning up deselected providers...'));
288
+ for (const providerName of deselected) {
289
+ const adapter = agentManager.getAdapter(providerName);
290
+ if (adapter) {
291
+ const removed = await adapter.removeAllCommands();
292
+ this.log(chalk.gray(` ✓ Removed ${removed} command(s) from ${adapter.displayName}`));
293
+ }
294
+ }
295
+ }
296
+ }
297
+ // Update config
298
+ config.providers = newProviders;
299
+ this.saveConfig(configPath, config);
300
+ // Prompt to run update
301
+ const { runUpdate } = await inquirer.prompt([
302
+ {
303
+ type: 'confirm',
304
+ name: 'runUpdate',
305
+ message: 'Run update to regenerate all commands?',
306
+ default: true,
307
+ },
308
+ ]);
309
+ if (runUpdate) {
310
+ this.log(chalk.gray('\n🔧 Regenerating commands for all providers...\n'));
311
+ const Update = (await import('./update.js')).default;
312
+ await Update.run([]);
313
+ }
314
+ this.log(chalk.green('\n✅ Providers replaced successfully!'));
107
315
  }
108
316
  async getConfig(configPath, key) {
109
317
  const config = this.loadConfig(configPath);
@@ -153,57 +361,33 @@ export default class Config extends Command {
153
361
  return;
154
362
  }
155
363
  const config = this.loadConfig(configPath);
156
- const defaultConfig = this.getDefaultConfig(config.agent);
364
+ const defaultConfig = {
365
+ ...DEFAULT_CONFIG,
366
+ providers: config.providers, // Keep existing providers
367
+ };
157
368
  this.saveConfig(configPath, defaultConfig);
158
- this.log(chalk.green('✅ Configuration reset to defaults'));
159
- }
160
- async changeAgent(configPath, config) {
161
- const agentManager = new AgentManager();
162
- const availableAgents = await agentManager.detectAgents();
163
- if (availableAgents.length === 0) {
164
- this.error('No supported agents detected in this project');
165
- }
166
- const { newAgent } = await inquirer.prompt([
167
- {
168
- type: 'list',
169
- name: 'newAgent',
170
- message: 'Select agent:',
171
- choices: availableAgents.map(agent => ({
172
- name: agent.displayName,
173
- value: agent.name,
174
- })),
175
- default: config.agent,
176
- },
177
- ]);
178
- if (newAgent === config.agent) {
179
- this.log(chalk.gray('No changes made'));
180
- return;
181
- }
182
- config.agent = newAgent;
183
- this.saveConfig(configPath, config);
184
- this.log(chalk.green(`✅ Agent changed to ${newAgent}`));
185
- this.log(chalk.yellow('\n⚠️ Run ') + chalk.cyan('clavix update') + chalk.yellow(' to update slash commands'));
369
+ this.log(chalk.green('✅ Configuration reset to defaults (providers preserved)'));
186
370
  }
187
371
  async editPreferences(configPath, config) {
188
- const preferences = config.preferences || {};
372
+ const preferences = config.preferences || DEFAULT_CONFIG.preferences;
189
373
  const answers = await inquirer.prompt([
190
374
  {
191
375
  type: 'confirm',
192
376
  name: 'autoOpenOutputs',
193
377
  message: 'Auto-open generated outputs?',
194
- default: preferences.autoOpenOutputs || false,
378
+ default: preferences.autoOpenOutputs,
195
379
  },
196
380
  {
197
381
  type: 'confirm',
198
382
  name: 'verboseLogging',
199
383
  message: 'Enable verbose logging?',
200
- default: preferences.verboseLogging || false,
384
+ default: preferences.verboseLogging,
201
385
  },
202
386
  {
203
387
  type: 'confirm',
204
388
  name: 'preserveSessions',
205
389
  message: 'Preserve completed sessions?',
206
- default: preferences.preserveSessions !== false, // Default to true
390
+ default: preferences.preserveSessions,
207
391
  },
208
392
  ]);
209
393
  config.preferences = answers;
@@ -212,7 +396,15 @@ export default class Config extends Command {
212
396
  }
213
397
  loadConfig(configPath) {
214
398
  try {
215
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
399
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
400
+ // Check if legacy config and migrate
401
+ if (isLegacyConfig(rawConfig)) {
402
+ this.warn(chalk.yellow('Detected legacy config format. Migrating to new format...'));
403
+ const migratedConfig = migrateConfig(rawConfig);
404
+ this.saveConfig(configPath, migratedConfig);
405
+ return migratedConfig;
406
+ }
407
+ return rawConfig;
216
408
  }
217
409
  catch (error) {
218
410
  this.error(chalk.red(`Failed to load configuration: ${error.message}`));
@@ -228,17 +420,17 @@ export default class Config extends Command {
228
420
  }
229
421
  displayConfig(config) {
230
422
  this.log(` ${chalk.gray('Version:')} ${config.version}`);
231
- this.log(` ${chalk.gray('Agent:')} ${chalk.cyan(config.agent)}`);
423
+ this.log(` ${chalk.gray('Providers:')} ${config.providers.map(p => chalk.cyan(p)).join(', ') || chalk.gray('(none)')}`);
232
424
  if (config.preferences) {
233
425
  this.log(`\n ${chalk.bold('Preferences:')}`);
234
426
  this.log(` ${chalk.gray('Auto-open outputs:')} ${config.preferences.autoOpenOutputs ? chalk.green('yes') : chalk.gray('no')}`);
235
427
  this.log(` ${chalk.gray('Verbose logging:')} ${config.preferences.verboseLogging ? chalk.green('yes') : chalk.gray('no')}`);
236
- this.log(` ${chalk.gray('Preserve sessions:')} ${config.preferences.preserveSessions !== false ? chalk.green('yes') : chalk.gray('no')}`);
428
+ this.log(` ${chalk.gray('Preserve sessions:')} ${config.preferences.preserveSessions ? chalk.green('yes') : chalk.gray('no')}`);
237
429
  }
238
430
  if (config.outputs) {
239
431
  this.log(`\n ${chalk.bold('Outputs:')}`);
240
- this.log(` ${chalk.gray('Path:')} ${config.outputs.path || '.clavix/outputs'}`);
241
- this.log(` ${chalk.gray('Format:')} ${config.outputs.format || 'markdown'}`);
432
+ this.log(` ${chalk.gray('Path:')} ${config.outputs.path}`);
433
+ this.log(` ${chalk.gray('Format:')} ${config.outputs.format}`);
242
434
  }
243
435
  this.log('');
244
436
  }
@@ -260,26 +452,5 @@ export default class Config extends Command {
260
452
  }, obj);
261
453
  target[lastKey] = value;
262
454
  }
263
- getDefaultConfig(agent) {
264
- return {
265
- version: '1.0.0',
266
- agent,
267
- templates: {
268
- prdQuestions: 'default',
269
- fullPrd: 'default',
270
- quickPrd: 'default',
271
- },
272
- outputs: {
273
- path: '.clavix/outputs',
274
- format: 'markdown',
275
- },
276
- preferences: {
277
- autoOpenOutputs: false,
278
- verboseLogging: false,
279
- preserveSessions: true,
280
- },
281
- experimental: {},
282
- };
283
- }
284
455
  }
285
456
  //# sourceMappingURL=config.js.map
@@ -38,115 +38,67 @@ export default class Init extends Command {
38
38
  return;
39
39
  }
40
40
  }
41
- // Select providers (multi-select)
41
+ // Load existing config if re-initializing
42
42
  const agentManager = new AgentManager();
43
+ let existingProviders = [];
44
+ if (await FileSystem.exists('.clavix/config.json')) {
45
+ try {
46
+ const configContent = await FileSystem.readFile('.clavix/config.json');
47
+ const config = JSON5.parse(configContent);
48
+ existingProviders = config.providers || [];
49
+ }
50
+ catch (error) {
51
+ // Ignore parse errors, will use empty array
52
+ }
53
+ }
54
+ // Select providers using shared utility
43
55
  console.log(chalk.gray('Select AI development tools to support:\n'));
44
56
  console.log(chalk.gray('(Space to select, Enter to confirm)\n'));
45
- const { selectedProviders } = await inquirer.prompt([
46
- {
47
- type: 'checkbox',
48
- name: 'selectedProviders',
49
- message: 'Which AI tools are you using?',
50
- choices: [
51
- // CLI Tools
52
- {
53
- name: 'Amp (.agents/commands/)',
54
- value: 'amp',
55
- },
56
- {
57
- name: 'Augment CLI (.augment/commands/clavix/)',
58
- value: 'augment',
59
- },
60
- {
61
- name: 'Codex CLI (~/.codex/prompts)',
62
- value: 'codex',
63
- },
64
- {
65
- name: 'CodeBuddy (.codebuddy/commands/)',
66
- value: 'codebuddy',
67
- },
68
- {
69
- name: 'Crush CLI (.crush/commands/clavix/)',
70
- value: 'crush',
71
- },
72
- {
73
- name: 'Claude Code (.claude/commands/clavix/)',
74
- value: 'claude-code',
75
- },
76
- {
77
- name: 'Droid CLI (.factory/commands/)',
78
- value: 'droid',
79
- },
80
- {
81
- name: 'Gemini CLI (.gemini/commands/clavix/)',
82
- value: 'gemini',
83
- },
84
- {
85
- name: 'LLXPRT (.llxprt/commands/clavix/)',
86
- value: 'llxprt',
87
- },
88
- {
89
- name: 'OpenCode (.opencode/command/)',
90
- value: 'opencode',
91
- },
92
- {
93
- name: 'Qwen Code (.qwen/commands/clavix/)',
94
- value: 'qwen',
95
- },
96
- new inquirer.Separator(),
97
- // IDE & IDE Extensions
98
- {
99
- name: 'Cursor (.cursor/commands/)',
100
- value: 'cursor',
101
- },
102
- {
103
- name: 'Windsurf (.windsurf/workflows/)',
104
- value: 'windsurf',
105
- },
106
- {
107
- name: 'Kilocode (.kilocode/workflows/)',
108
- value: 'kilocode',
109
- },
110
- {
111
- name: 'Roocode (.roo/commands/)',
112
- value: 'roocode',
113
- },
114
- {
115
- name: 'Cline (.clinerules/workflows/)',
116
- value: 'cline',
117
- },
118
- new inquirer.Separator(),
119
- // Universal Adapters
120
- {
121
- name: 'Agents (AGENTS.md - Universal - for tools without slash commands)',
122
- value: 'agents-md',
123
- },
124
- {
125
- name: 'GitHub Copilot (.github/copilot-instructions.md)',
126
- value: 'copilot-instructions',
127
- },
128
- {
129
- name: 'Warp (WARP.md - optimized for Warp)',
130
- value: 'warp-md',
131
- },
132
- {
133
- name: 'Octofriend (OCTO.md - optimized for Octofriend)',
134
- value: 'octo-md',
135
- },
136
- new inquirer.Separator(),
137
- ],
138
- validate: (answer) => {
139
- if (answer.length === 0) {
140
- return 'You must select at least one provider.';
141
- }
142
- return true;
143
- },
144
- },
145
- ]);
57
+ const { selectProviders } = await import('../../utils/provider-selector.js');
58
+ const selectedProviders = await selectProviders(agentManager, existingProviders);
146
59
  if (!selectedProviders || selectedProviders.length === 0) {
147
60
  console.log(chalk.red('\n✗ No providers selected\n'));
148
61
  return;
149
62
  }
63
+ // Handle deselected providers (cleanup prompt)
64
+ const deselectedProviders = existingProviders.filter((p) => !selectedProviders.includes(p));
65
+ if (deselectedProviders.length > 0) {
66
+ console.log(chalk.yellow('\n⚠ Previously configured but not selected:'));
67
+ for (const providerName of deselectedProviders) {
68
+ const adapter = agentManager.getAdapter(providerName);
69
+ const displayName = adapter?.displayName || providerName;
70
+ const directory = adapter?.directory || 'unknown';
71
+ console.log(chalk.gray(` • ${displayName} (${directory})`));
72
+ }
73
+ const { cleanupAction } = await inquirer.prompt([
74
+ {
75
+ type: 'list',
76
+ name: 'cleanupAction',
77
+ message: 'What would you like to do with these providers?',
78
+ choices: [
79
+ { name: 'Clean up (remove all command files)', value: 'cleanup' },
80
+ { name: 'Keep (also update their commands)', value: 'update' },
81
+ { name: 'Skip (leave as-is)', value: 'skip' },
82
+ ],
83
+ },
84
+ ]);
85
+ if (cleanupAction === 'cleanup') {
86
+ console.log(chalk.gray('\n🗑️ Cleaning up deselected providers...'));
87
+ for (const providerName of deselectedProviders) {
88
+ const adapter = agentManager.getAdapter(providerName);
89
+ if (adapter) {
90
+ const removed = await adapter.removeAllCommands();
91
+ console.log(chalk.gray(` ✓ Removed ${removed} command(s) from ${adapter.displayName}`));
92
+ }
93
+ }
94
+ }
95
+ else if (cleanupAction === 'update') {
96
+ // Add them back to selection
97
+ selectedProviders.push(...deselectedProviders);
98
+ console.log(chalk.gray('\n✓ Keeping all providers\n'));
99
+ }
100
+ // If 'skip': do nothing
101
+ }
150
102
  // Create .clavix directory structure
151
103
  console.log(chalk.cyan('\n📁 Creating directory structure...'));
152
104
  await this.createDirectoryStructure();
@@ -235,6 +187,11 @@ export default class Init extends Command {
235
187
  }
236
188
  }
237
189
  }
190
+ // Remove all existing commands before regenerating (ensures clean state)
191
+ const removed = await adapter.removeAllCommands();
192
+ if (removed > 0) {
193
+ console.log(chalk.gray(` Removed ${removed} existing command(s)`));
194
+ }
238
195
  // Generate slash commands
239
196
  const generatedTemplates = await this.generateSlashCommands(adapter);
240
197
  await this.handleLegacyCommands(adapter, generatedTemplates);
@@ -295,7 +252,7 @@ export default class Init extends Command {
295
252
  providers,
296
253
  };
297
254
  const configPath = '.clavix/config.json';
298
- const configContent = JSON5.stringify(config, null, 2);
255
+ const configContent = JSON.stringify(config, null, 2);
299
256
  await FileSystem.writeFileAtomic(configPath, configContent);
300
257
  }
301
258
  async generateInstructions() {
@@ -357,6 +314,8 @@ Welcome to Clavix! This directory contains your local Clavix configuration and d
357
314
 
358
315
  If using Claude Code, Cursor, or Windsurf, the following slash commands are available:
359
316
 
317
+ **Note:** Running \`clavix init\` or \`clavix update\` will regenerate all slash commands from templates. Any manual edits to generated commands will be lost. If you need custom commands, create new command files instead of modifying generated ones.
318
+
360
319
  ### Prompt Improvement
361
320
  - \`/clavix:fast [prompt]\` - Quick prompt improvements
362
321
  - \`/clavix:deep [prompt]\` - Comprehensive prompt analysis
@@ -146,53 +146,25 @@ export default class Update extends Command {
146
146
  }
147
147
  async updateCommands(adapter, force) {
148
148
  this.log(chalk.cyan(`\n🔧 Updating slash commands for ${adapter.displayName}...`));
149
- const commandsDir = adapter.getCommandPath();
150
- const commandsPath = path.join(process.cwd(), commandsDir);
151
- const extension = adapter.fileExtension;
152
- // Dynamically scan template directory for all command templates
153
- const templatesDir = path.join(__dirname, '..', '..', 'templates', 'slash-commands', adapter.name);
154
- if (!fs.existsSync(templatesDir)) {
155
- this.log(chalk.yellow(` ⚠ Templates directory not found: ${templatesDir}`));
156
- return 0;
149
+ // Remove all existing commands first (force regeneration)
150
+ const removed = await adapter.removeAllCommands();
151
+ if (removed > 0) {
152
+ this.log(chalk.gray(` Removed ${removed} existing command(s)`));
157
153
  }
158
- // Get all .md template files
159
- const templateFiles = fs.readdirSync(templatesDir)
160
- .filter(file => file.endsWith(extension))
161
- .map(file => file.slice(0, -extension.length));
162
- if (templateFiles.length === 0) {
154
+ // Load templates using the canonical template loader
155
+ const { loadCommandTemplates } = await import('../../utils/template-loader.js');
156
+ const templates = await loadCommandTemplates(adapter);
157
+ if (templates.length === 0) {
163
158
  this.log(chalk.yellow(' ⚠ No command templates found'));
164
159
  return 0;
165
160
  }
166
- // Ensure commands directory exists
167
- if (!fs.existsSync(commandsPath)) {
168
- fs.mkdirpSync(commandsPath);
169
- this.log(chalk.gray(` ✓ Created commands directory: ${commandsDir}`));
170
- }
171
- let updated = 0;
172
- for (const command of templateFiles) {
173
- const filename = adapter.getTargetFilename(command);
174
- const commandFile = path.join(commandsPath, filename);
175
- const templatePath = path.join(templatesDir, `${command}${extension}`);
176
- const newContent = fs.readFileSync(templatePath, 'utf-8');
177
- if (fs.existsSync(commandFile)) {
178
- const currentContent = fs.readFileSync(commandFile, 'utf-8');
179
- if (force || currentContent !== newContent) {
180
- fs.writeFileSync(commandFile, newContent);
181
- this.log(chalk.gray(` ✓ Updated ${filename}`));
182
- updated++;
183
- }
184
- else {
185
- this.log(chalk.gray(` • ${filename} already up to date`));
186
- }
187
- }
188
- else {
189
- fs.writeFileSync(commandFile, newContent);
190
- this.log(chalk.gray(` ✓ Created ${filename}`));
191
- updated++;
192
- }
193
- }
194
- updated += await this.handleLegacyCommands(adapter, templateFiles, force);
195
- return updated;
161
+ // Generate fresh commands from templates
162
+ await adapter.generateCommands(templates);
163
+ this.log(chalk.gray(` ✓ Generated ${templates.length} command(s)`));
164
+ // Handle legacy commands (cleanup old naming patterns)
165
+ const commandNames = templates.map(t => t.name);
166
+ const legacyRemoved = await this.handleLegacyCommands(adapter, commandNames, force);
167
+ return removed + templates.length + legacyRemoved;
196
168
  }
197
169
  async handleLegacyCommands(adapter, commandNames, force) {
198
170
  if (commandNames.length === 0) {
@@ -21,6 +21,19 @@ export declare abstract class BaseAdapter implements AgentAdapter {
21
21
  * Checks if directory can be created and is writable
22
22
  */
23
23
  validate(): Promise<ValidationResult>;
24
+ /**
25
+ * Remove all existing Clavix-generated commands for this adapter
26
+ * Called before regenerating to ensure clean state
27
+ * @returns Number of files removed
28
+ */
29
+ removeAllCommands(): Promise<number>;
30
+ /**
31
+ * Determine if a file is a Clavix-generated command
32
+ * Override in adapters for provider-specific patterns
33
+ * @param filename The filename to check
34
+ * @returns true if this is a Clavix-generated command file
35
+ */
36
+ protected isClavixGeneratedCommand(filename: string): boolean;
24
37
  /**
25
38
  * Generate commands - default implementation
26
39
  * Creates command files in the provider's directory
@@ -59,6 +59,43 @@ export class BaseAdapter {
59
59
  warnings: warnings.length > 0 ? warnings : undefined,
60
60
  };
61
61
  }
62
+ /**
63
+ * Remove all existing Clavix-generated commands for this adapter
64
+ * Called before regenerating to ensure clean state
65
+ * @returns Number of files removed
66
+ */
67
+ async removeAllCommands() {
68
+ const commandPath = this.getCommandPath();
69
+ // If directory doesn't exist, nothing to remove
70
+ if (!(await FileSystem.exists(commandPath))) {
71
+ return 0;
72
+ }
73
+ const files = await FileSystem.listFiles(commandPath);
74
+ const clavixCommands = files.filter((f) => this.isClavixGeneratedCommand(f));
75
+ let removed = 0;
76
+ for (const file of clavixCommands) {
77
+ const filePath = path.join(commandPath, file);
78
+ try {
79
+ await FileSystem.remove(filePath);
80
+ removed++;
81
+ }
82
+ catch (error) {
83
+ // Log warning but continue with other files
84
+ console.warn(`Failed to remove ${filePath}: ${error}`);
85
+ }
86
+ }
87
+ return removed;
88
+ }
89
+ /**
90
+ * Determine if a file is a Clavix-generated command
91
+ * Override in adapters for provider-specific patterns
92
+ * @param filename The filename to check
93
+ * @returns true if this is a Clavix-generated command file
94
+ */
95
+ isClavixGeneratedCommand(filename) {
96
+ // Default: match files with our extension
97
+ return filename.endsWith(this.fileExtension);
98
+ }
62
99
  /**
63
100
  * Generate commands - default implementation
64
101
  * Creates command files in the provider's directory
@@ -20,6 +20,11 @@ export declare class ClaudeCodeAdapter extends BaseAdapter {
20
20
  * Get command path for Claude Code
21
21
  */
22
22
  getCommandPath(): string;
23
+ /**
24
+ * Determine if a file is a Clavix-generated command
25
+ * For Claude Code: Any .md file in .claude/commands/clavix/ is ours
26
+ */
27
+ protected isClavixGeneratedCommand(filename: string): boolean;
23
28
  /**
24
29
  * Inject documentation blocks into CLAUDE.md
25
30
  */
@@ -27,6 +27,15 @@ export class ClaudeCodeAdapter extends BaseAdapter {
27
27
  getCommandPath() {
28
28
  return this.directory;
29
29
  }
30
+ /**
31
+ * Determine if a file is a Clavix-generated command
32
+ * For Claude Code: Any .md file in .claude/commands/clavix/ is ours
33
+ */
34
+ isClavixGeneratedCommand(filename) {
35
+ // Only remove .md files (our slash commands)
36
+ // This is safe because we control the entire .claude/commands/clavix/ directory
37
+ return filename.endsWith('.md');
38
+ }
30
39
  // generateCommands is inherited from BaseAdapter
31
40
  /**
32
41
  * Inject documentation blocks into CLAUDE.md
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { run, handle, settings } from '@oclif/core';
3
+ // Disable debug mode (stack traces) unless explicitly requested via DEBUG env var
4
+ if (!process.env.DEBUG) {
5
+ settings.debug = false;
6
+ }
7
+ // Run if called directly
8
+ if (import.meta.url === `file://${process.argv[1]}`) {
9
+ run().catch(handle);
10
+ }
11
+ // Export for testing
12
+ export { run };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '@oclif/core';
3
+ export { run };
4
+ //# sourceMappingURL=index.d.ts.map
@@ -9,6 +9,7 @@ export interface AgentAdapter {
9
9
  features?: ProviderFeatures;
10
10
  detectProject(): Promise<boolean>;
11
11
  generateCommands(templates: CommandTemplate[]): Promise<void>;
12
+ removeAllCommands(): Promise<number>;
12
13
  injectDocumentation(blocks: ManagedBlock[]): Promise<void>;
13
14
  getCommandPath(): string;
14
15
  getTargetFilename(name: string): string;
@@ -0,0 +1,8 @@
1
+ import { AgentManager } from '../core/agent-manager.js';
2
+ /**
3
+ * Interactive provider selection utility
4
+ * Displays multi-select checkbox for all available providers
5
+ * Used by both init and config commands
6
+ */
7
+ export declare function selectProviders(agentManager: AgentManager, preSelected?: string[]): Promise<string[]>;
8
+ //# sourceMappingURL=provider-selector.d.ts.map
@@ -0,0 +1,60 @@
1
+ import inquirer from 'inquirer';
2
+ /**
3
+ * Interactive provider selection utility
4
+ * Displays multi-select checkbox for all available providers
5
+ * Used by both init and config commands
6
+ */
7
+ export async function selectProviders(agentManager, preSelected = []) {
8
+ const { selectedProviders } = await inquirer.prompt([
9
+ {
10
+ type: 'checkbox',
11
+ name: 'selectedProviders',
12
+ message: 'Which AI tools are you using?',
13
+ choices: [
14
+ new inquirer.Separator('=== CLI Tools ==='),
15
+ { name: 'Amp (.agents/commands/)', value: 'amp' },
16
+ { name: 'Augment CLI (.augment/commands/clavix/)', value: 'augment' },
17
+ { name: 'Codex CLI (~/.codex/prompts)', value: 'codex' },
18
+ { name: 'Droid (.droid/clavix/)', value: 'droid' },
19
+ { name: 'Gemini CLI (.gemini/commands/clavix/)', value: 'gemini' },
20
+ { name: 'Kilocode (.kilo/clavix/)', value: 'kilocode' },
21
+ { name: 'LLXPRT CLI (.llxprt/clavix/)', value: 'llxprt' },
22
+ { name: 'OpenCode (.opencode/clavix/)', value: 'opencode' },
23
+ { name: 'Qwen (通义灵码) (~/.qwen/commands/clavix/)', value: 'qwen' },
24
+ { name: 'RooCode (.roo/clavix/)', value: 'roocode' },
25
+ new inquirer.Separator(),
26
+ new inquirer.Separator('=== IDE & IDE Extensions ==='),
27
+ { name: 'Claude Code (.claude/commands/clavix/)', value: 'claude-code' },
28
+ { name: 'Cline (.cline/workflows/)', value: 'cline' },
29
+ { name: 'CodeBuddy (.codebuddy/prompts/)', value: 'codebuddy' },
30
+ { name: 'Copilot Instructions (.github/copilot-instructions.md)', value: 'copilot-instructions' },
31
+ { name: 'Crush (crush://prompts)', value: 'crush' },
32
+ { name: 'Cursor (.cursor/commands/)', value: 'cursor' },
33
+ { name: 'Windsurf (.windsurf/rules/)', value: 'windsurf' },
34
+ new inquirer.Separator(),
35
+ new inquirer.Separator('=== Universal Adapters ==='),
36
+ { name: 'Agents (AGENTS.md - Universal)', value: 'agents-md' },
37
+ { name: 'Octo (OCTO.md - Universal)', value: 'octo-md' },
38
+ { name: 'Custom (custom/ directory)', value: 'custom' },
39
+ ].map((choice) => {
40
+ // Keep separators as-is
41
+ if (choice instanceof inquirer.Separator) {
42
+ return choice;
43
+ }
44
+ // Add 'checked' property based on preSelected
45
+ return {
46
+ ...choice,
47
+ checked: preSelected.includes(choice.value),
48
+ };
49
+ }),
50
+ validate: (answer) => {
51
+ if (answer.length === 0) {
52
+ return 'You must select at least one provider.';
53
+ }
54
+ return true;
55
+ },
56
+ },
57
+ ]);
58
+ return selectedProviders;
59
+ }
60
+ //# sourceMappingURL=provider-selector.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavix",
3
- "version": "3.2.2",
3
+ "version": "3.3.1",
4
4
  "description": "Clavix Intelligence™ for AI coding. Automatically optimizes prompts with intent detection, quality assessment, and adaptive patterns—no framework to learn. Works with Claude Code, Cursor, Windsurf, and 19+ other AI coding tools.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",