archicore 0.2.1 → 0.2.3

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.
@@ -63,22 +63,38 @@ export async function initProject(dir, name) {
63
63
  const spinner = createSpinner('Initializing ArchiCore...').start();
64
64
  try {
65
65
  const config = await loadConfig();
66
- // Create project on server
67
- const response = await fetch(`${config.serverUrl}/api/projects`, {
68
- method: 'POST',
69
- headers: { 'Content-Type': 'application/json' },
70
- body: JSON.stringify({ name: projectName, path: dir }),
71
- });
72
66
  let projectId;
73
67
  let serverName = projectName;
74
- if (response.ok) {
75
- const data = await response.json();
76
- projectId = data.id || data.project?.id;
77
- serverName = data.name || data.project?.name || projectName;
68
+ let serverAvailable = false;
69
+ // Try to create project on server (only if we have a token)
70
+ try {
71
+ const headers = { 'Content-Type': 'application/json' };
72
+ if (config.accessToken) {
73
+ headers['Authorization'] = `Bearer ${config.accessToken}`;
74
+ }
75
+ const response = await fetch(`${config.serverUrl}/api/projects`, {
76
+ method: 'POST',
77
+ headers,
78
+ body: JSON.stringify({ name: projectName, path: dir }),
79
+ });
80
+ if (response.ok) {
81
+ const data = await response.json();
82
+ projectId = data.id || data.project?.id;
83
+ serverName = data.name || data.project?.name || projectName;
84
+ serverAvailable = true;
85
+ }
86
+ else if (response.status === 401 || response.status === 403) {
87
+ // Not authenticated - that's OK, will sync on first /index
88
+ spinner.update('Creating local config (login required for sync)...');
89
+ serverAvailable = true; // Server is available, just not authenticated
90
+ }
91
+ else {
92
+ spinner.warn('Server returned error, creating local config only');
93
+ }
78
94
  }
79
- else {
80
- // Server might not be running, that's OK - we can still create local config
81
- spinner.warn('Server not available, creating local config only');
95
+ catch (fetchError) {
96
+ // Network error - server not reachable
97
+ spinner.warn('Server not reachable, creating local config only');
82
98
  }
83
99
  // Create local config
84
100
  const localConfig = {
@@ -91,8 +107,15 @@ export async function initProject(dir, name) {
91
107
  await saveLocalProject(dir, localConfig);
92
108
  // Add .archicore to .gitignore if git repo
93
109
  await addToGitignore(dir);
110
+ // Show appropriate success message
94
111
  if (projectId) {
95
- spinner.succeed('ArchiCore initialized');
112
+ spinner.succeed('ArchiCore initialized and synced');
113
+ }
114
+ else if (serverAvailable) {
115
+ spinner.succeed('ArchiCore initialized (will sync after login)');
116
+ }
117
+ else {
118
+ spinner.succeed('ArchiCore initialized locally');
96
119
  }
97
120
  console.log();
98
121
  printKeyValue('Directory', dir);
@@ -10,6 +10,25 @@ import { printFormattedError, printStartupError, } from '../utils/error-handler.
10
10
  import { isInitialized, getLocalProject } from './init.js';
11
11
  import { requireAuth, logout } from './auth.js';
12
12
  import { colors, icons, createSpinner, printHelp, printGoodbye, printSection, printSuccess, printError, printWarning, printInfo, printKeyValue, header, } from '../ui/index.js';
13
+ // Command registry with descriptions (like Claude CLI)
14
+ const COMMANDS = [
15
+ { name: 'index', aliases: ['i'], description: 'Index your codebase for analysis' },
16
+ { name: 'analyze', aliases: ['impact'], description: 'Analyze impact of changes' },
17
+ { name: 'search', aliases: ['s'], description: 'Search code semantically' },
18
+ { name: 'dead-code', aliases: ['deadcode'], description: 'Find unused code' },
19
+ { name: 'security', aliases: ['sec'], description: 'Scan for security vulnerabilities' },
20
+ { name: 'metrics', aliases: ['stats'], description: 'Show code metrics and statistics' },
21
+ { name: 'duplication', aliases: ['duplicates'], description: 'Detect code duplication' },
22
+ { name: 'refactoring', aliases: ['refactor'], description: 'Get refactoring suggestions' },
23
+ { name: 'rules', aliases: [], description: 'Check architectural rules' },
24
+ { name: 'docs', aliases: ['documentation'], description: 'Generate architecture documentation' },
25
+ { name: 'export', aliases: [], description: 'Export analysis results' },
26
+ { name: 'status', aliases: [], description: 'Show connection and project status' },
27
+ { name: 'clear', aliases: ['cls'], description: 'Clear the screen' },
28
+ { name: 'help', aliases: ['h'], description: 'Show available commands' },
29
+ { name: 'logout', aliases: [], description: 'Log out from ArchiCore' },
30
+ { name: 'exit', aliases: ['quit', 'q'], description: 'Exit ArchiCore CLI' },
31
+ ];
13
32
  const state = {
14
33
  running: true,
15
34
  projectId: null,
@@ -113,29 +132,67 @@ export async function startInteractiveMode() {
113
132
  console.log(colors.muted(` Project: ${colors.primary(state.projectName)}`));
114
133
  }
115
134
  console.log();
116
- console.log(colors.muted(' Type /help for commands or ask a question about your code.'));
135
+ console.log(colors.muted(' Type / to see commands, or ask a question about your code.'));
136
+ console.log(colors.muted(' Press Tab to autocomplete. Type ? for shortcuts.'));
117
137
  console.log();
118
- // Start REPL
119
- const rl = readline.createInterface({
120
- input: process.stdin,
121
- output: process.stdout,
122
- terminal: true,
123
- });
124
- // Keep the process alive
125
- rl.ref?.();
126
- const prompt = () => {
127
- const prefix = state.projectName
138
+ // Interactive input with real-time autocomplete
139
+ await startInteractiveInput(state);
140
+ }
141
+ /**
142
+ * Start interactive input loop with real-time command suggestions
143
+ */
144
+ async function startInteractiveInput(state) {
145
+ const getPromptPrefix = () => {
146
+ return state.projectName
128
147
  ? colors.muted(`[${state.projectName}] `)
129
148
  : '';
130
- process.stdout.write(`${prefix}${colors.primary(icons.arrow)} `);
131
149
  };
132
- prompt();
133
- const processLine = async (input) => {
134
- const trimmed = input.trim();
135
- if (!trimmed) {
136
- prompt();
150
+ const promptStr = () => `${getPromptPrefix()}${colors.primary(icons.arrow)} `;
151
+ // State for current input
152
+ let currentInput = '';
153
+ let menuVisible = false;
154
+ let selectedIndex = 0;
155
+ let filteredCommands = [];
156
+ // Filter commands based on input
157
+ const filterCommands = (input) => {
158
+ if (!input.startsWith('/'))
159
+ return [];
160
+ const query = input.slice(1).toLowerCase();
161
+ if (!query)
162
+ return COMMANDS.slice(0, 10);
163
+ return COMMANDS.filter(cmd => cmd.name.toLowerCase().startsWith(query) ||
164
+ cmd.aliases.some(a => a.toLowerCase().startsWith(query))).slice(0, 10);
165
+ };
166
+ // Render the menu
167
+ const renderMenu = () => {
168
+ if (filteredCommands.length === 0)
137
169
  return;
170
+ console.log();
171
+ for (let i = 0; i < filteredCommands.length; i++) {
172
+ const cmd = filteredCommands[i];
173
+ const isSelected = i === selectedIndex;
174
+ const nameStr = `/${cmd.name}`.padEnd(18);
175
+ if (isSelected) {
176
+ console.log(` ${colors.highlight(nameStr)} ${colors.muted(cmd.description)}`);
177
+ }
178
+ else {
179
+ console.log(` ${colors.primary(nameStr)} ${colors.dim(cmd.description)}`);
180
+ }
138
181
  }
182
+ console.log(colors.dim(' ↑↓ select • Tab complete • Enter run'));
183
+ };
184
+ // Clear menu lines
185
+ const clearMenu = (lines) => {
186
+ for (let i = 0; i < lines; i++) {
187
+ readline.moveCursor(process.stdout, 0, -1);
188
+ readline.clearLine(process.stdout, 0);
189
+ }
190
+ };
191
+ // Process submitted input
192
+ const processInput = async (input) => {
193
+ const trimmed = input.trim();
194
+ if (!trimmed)
195
+ return;
139
196
  state.history.push(trimmed);
140
197
  try {
141
198
  await handleInput(trimmed);
@@ -143,61 +200,206 @@ export async function startInteractiveMode() {
143
200
  catch (error) {
144
201
  printError(String(error));
145
202
  }
146
- if (state.running) {
203
+ };
204
+ // Use raw mode for real-time input
205
+ if (process.stdin.isTTY) {
206
+ readline.emitKeypressEvents(process.stdin);
207
+ process.stdin.setRawMode(true);
208
+ }
209
+ process.stdout.write(promptStr());
210
+ const handleKeypress = async (str, key) => {
211
+ // Handle Ctrl+C
212
+ if (key.ctrl && key.name === 'c') {
213
+ if (menuVisible) {
214
+ clearMenu(filteredCommands.length + 2);
215
+ menuVisible = false;
216
+ }
147
217
  console.log();
148
- prompt();
149
- }
150
- else {
151
- rl.close();
218
+ state.running = false;
219
+ printGoodbye();
220
+ process.exit(0);
152
221
  }
153
- };
154
- rl.on('line', (input) => {
155
- processLine(input).catch((err) => {
156
- printError(String(err));
157
- prompt();
158
- });
159
- });
160
- rl.on('close', () => {
161
- if (state.running) {
162
- // Unexpected close - don't exit, restart readline
222
+ // Handle Enter
223
+ if (key.name === 'return') {
224
+ if (menuVisible && filteredCommands.length > 0) {
225
+ // Select from menu
226
+ clearMenu(filteredCommands.length + 2);
227
+ menuVisible = false;
228
+ const selected = filteredCommands[selectedIndex];
229
+ currentInput = '/' + selected.name;
230
+ readline.clearLine(process.stdout, 0);
231
+ readline.cursorTo(process.stdout, 0);
232
+ process.stdout.write(promptStr() + currentInput);
233
+ }
234
+ // Submit
235
+ if (menuVisible) {
236
+ clearMenu(filteredCommands.length + 2);
237
+ menuVisible = false;
238
+ }
163
239
  console.log();
164
- printWarning('Input stream interrupted, restarting...');
165
- // Recreate readline interface
166
- const newRl = readline.createInterface({
167
- input: process.stdin,
168
- output: process.stdout,
169
- terminal: true,
170
- });
171
- newRl.on('line', (input) => {
172
- processLine(input).catch((err) => {
173
- printError(String(err));
174
- prompt();
175
- });
176
- });
177
- newRl.on('close', () => {
178
- if (!state.running) {
179
- printGoodbye();
180
- process.exit(0);
240
+ await processInput(currentInput);
241
+ currentInput = '';
242
+ selectedIndex = 0;
243
+ filteredCommands = [];
244
+ if (state.running) {
245
+ console.log();
246
+ process.stdout.write(promptStr());
247
+ }
248
+ else {
249
+ process.stdin.setRawMode(false);
250
+ process.exit(0);
251
+ }
252
+ return;
253
+ }
254
+ // Handle Tab - autocomplete
255
+ if (key.name === 'tab') {
256
+ if (filteredCommands.length > 0) {
257
+ if (menuVisible) {
258
+ clearMenu(filteredCommands.length + 2);
181
259
  }
182
- });
183
- console.log();
184
- prompt();
260
+ const selected = filteredCommands[selectedIndex];
261
+ currentInput = '/' + selected.name + ' ';
262
+ menuVisible = false;
263
+ filteredCommands = [];
264
+ readline.clearLine(process.stdout, 0);
265
+ readline.cursorTo(process.stdout, 0);
266
+ process.stdout.write(promptStr() + currentInput);
267
+ }
268
+ return;
185
269
  }
186
- else {
187
- printGoodbye();
188
- process.exit(0);
270
+ // Handle Escape - close menu
271
+ if (key.name === 'escape') {
272
+ if (menuVisible) {
273
+ clearMenu(filteredCommands.length + 2);
274
+ menuVisible = false;
275
+ filteredCommands = [];
276
+ }
277
+ return;
189
278
  }
279
+ // Handle Up arrow
280
+ if (key.name === 'up') {
281
+ if (menuVisible && selectedIndex > 0) {
282
+ clearMenu(filteredCommands.length + 2);
283
+ selectedIndex--;
284
+ renderMenu();
285
+ }
286
+ return;
287
+ }
288
+ // Handle Down arrow
289
+ if (key.name === 'down') {
290
+ if (menuVisible && selectedIndex < filteredCommands.length - 1) {
291
+ clearMenu(filteredCommands.length + 2);
292
+ selectedIndex++;
293
+ renderMenu();
294
+ }
295
+ return;
296
+ }
297
+ // Handle Backspace
298
+ if (key.name === 'backspace') {
299
+ if (currentInput.length > 0) {
300
+ if (menuVisible) {
301
+ clearMenu(filteredCommands.length + 2);
302
+ }
303
+ currentInput = currentInput.slice(0, -1);
304
+ readline.clearLine(process.stdout, 0);
305
+ readline.cursorTo(process.stdout, 0);
306
+ process.stdout.write(promptStr() + currentInput);
307
+ // Update menu
308
+ filteredCommands = filterCommands(currentInput);
309
+ selectedIndex = 0;
310
+ menuVisible = currentInput.startsWith('/') && filteredCommands.length > 0;
311
+ if (menuVisible) {
312
+ renderMenu();
313
+ }
314
+ }
315
+ return;
316
+ }
317
+ // Regular character input
318
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
319
+ if (menuVisible) {
320
+ clearMenu(filteredCommands.length + 2);
321
+ }
322
+ currentInput += str;
323
+ readline.clearLine(process.stdout, 0);
324
+ readline.cursorTo(process.stdout, 0);
325
+ process.stdout.write(promptStr() + currentInput);
326
+ // Show menu when typing /
327
+ filteredCommands = filterCommands(currentInput);
328
+ selectedIndex = 0;
329
+ menuVisible = currentInput.startsWith('/') && filteredCommands.length > 0;
330
+ if (menuVisible) {
331
+ renderMenu();
332
+ }
333
+ }
334
+ };
335
+ process.stdin.on('keypress', handleKeypress);
336
+ // Keep process alive
337
+ await new Promise((resolve) => {
338
+ const checkRunning = setInterval(() => {
339
+ if (!state.running) {
340
+ clearInterval(checkRunning);
341
+ resolve();
342
+ }
343
+ }, 100);
190
344
  });
191
- // Handle SIGINT (Ctrl+C)
192
- process.on('SIGINT', () => {
193
- console.log();
194
- state.running = false;
195
- rl.close();
196
- });
345
+ }
346
+ /**
347
+ * Display command menu (like Claude CLI's / navigation)
348
+ */
349
+ function showCommandMenu(filter = '') {
350
+ const filterLower = filter.toLowerCase();
351
+ const filtered = COMMANDS.filter(cmd => cmd.name.includes(filterLower) ||
352
+ cmd.aliases.some(a => a.includes(filterLower)));
353
+ if (filtered.length === 0) {
354
+ console.log(colors.muted(' No matching commands'));
355
+ return;
356
+ }
357
+ console.log();
358
+ for (const cmd of filtered) {
359
+ const nameWidth = 20;
360
+ const paddedName = `/${cmd.name}`.padEnd(nameWidth);
361
+ console.log(` ${colors.primary(paddedName)} ${colors.muted(cmd.description)}`);
362
+ }
363
+ console.log();
364
+ console.log(colors.dim(' ? for shortcuts'));
197
365
  }
198
366
  async function handleInput(input) {
367
+ // Handle "?" for shortcuts
368
+ if (input === '?') {
369
+ console.log();
370
+ console.log(colors.highlight(' Keyboard Shortcuts:'));
371
+ console.log();
372
+ console.log(` ${colors.primary('Tab')} Autocomplete command`);
373
+ console.log(` ${colors.primary('/')} Show all commands`);
374
+ console.log(` ${colors.primary('/command')} Execute a command`);
375
+ console.log(` ${colors.primary('Ctrl+C')} Exit ArchiCore`);
376
+ console.log();
377
+ console.log(colors.highlight(' Quick Commands:'));
378
+ console.log();
379
+ console.log(` ${colors.primary('/i')} Index project (alias for /index)`);
380
+ console.log(` ${colors.primary('/s query')} Search code (alias for /search)`);
381
+ console.log(` ${colors.primary('/q')} Quit (alias for /exit)`);
382
+ console.log();
383
+ return;
384
+ }
199
385
  // Handle slash commands
200
386
  if (input.startsWith('/')) {
387
+ // If just "/" or "/?" - show command menu
388
+ if (input === '/' || input === '/?') {
389
+ showCommandMenu();
390
+ return;
391
+ }
392
+ // If partial command (e.g. "/in") - show filtered menu
393
+ const partialCmd = input.slice(1).split(/\s/)[0];
394
+ if (partialCmd && !input.includes(' ')) {
395
+ // Check if it's a complete command
396
+ const isComplete = COMMANDS.some(cmd => cmd.name === partialCmd || cmd.aliases.includes(partialCmd));
397
+ if (!isComplete) {
398
+ // Show filtered suggestions
399
+ showCommandMenu(partialCmd);
400
+ return;
401
+ }
402
+ }
201
403
  await handleCommand(input);
202
404
  return;
203
405
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Interactive Autocomplete Input for ArchiCore CLI
3
+ *
4
+ * Shows command suggestions in real-time as user types,
5
+ * similar to Claude's terminal interface.
6
+ *
7
+ * Works on Linux, Windows, and macOS.
8
+ */
9
+ import * as readline from 'readline';
10
+ export interface CommandItem {
11
+ name: string;
12
+ aliases: string[];
13
+ description: string;
14
+ }
15
+ export interface AutocompleteOptions {
16
+ commands: CommandItem[];
17
+ prompt?: string;
18
+ placeholder?: string;
19
+ }
20
+ /**
21
+ * Create an interactive input with autocomplete
22
+ */
23
+ export declare function createAutocompleteInput(options: AutocompleteOptions): Promise<string>;
24
+ /**
25
+ * Simple readline-based input with inline suggestions
26
+ * More compatible fallback for environments where raw mode doesn't work well
27
+ */
28
+ export declare function createSimpleAutocomplete(rl: readline.Interface, commands: CommandItem[], prompt: string): void;
29
+ //# sourceMappingURL=autocomplete.d.ts.map