@optiqcode/cli 1.0.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 +21 -0
- package/README.md +89 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +73 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +121 -0
- package/dist/commands/watch.d.ts +5 -0
- package/dist/commands/watch.js +156 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +498 -0
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.js +49 -0
- package/dist/utils/files.d.ts +7 -0
- package/dist/utils/files.js +103 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Optiq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Optiq CLI
|
|
2
|
+
|
|
3
|
+
Official CLI tool for [Optiq](https://optiqcode.com) - automatic code indexing and intelligent context engine for your codebase.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @optiqcode/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
1. **Login to Optiq**
|
|
14
|
+
```bash
|
|
15
|
+
optiq login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. **Index your codebase**
|
|
19
|
+
```bash
|
|
20
|
+
optiq index
|
|
21
|
+
# Or specify a directory
|
|
22
|
+
optiq index --path ./my-project
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. **Watch for changes** (optional)
|
|
26
|
+
```bash
|
|
27
|
+
optiq watch
|
|
28
|
+
# Or specify a directory
|
|
29
|
+
optiq watch --path ./my-project
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `optiq login`
|
|
35
|
+
Authenticate with your Optiq account. You'll need to provide your email and password.
|
|
36
|
+
|
|
37
|
+
### `optiq logout`
|
|
38
|
+
Sign out from your Optiq account.
|
|
39
|
+
|
|
40
|
+
### `optiq whoami`
|
|
41
|
+
Display your current logged-in account.
|
|
42
|
+
|
|
43
|
+
### `optiq index [options]`
|
|
44
|
+
Index your codebase for intelligent search and context retrieval.
|
|
45
|
+
|
|
46
|
+
**Options:**
|
|
47
|
+
- `--path <directory>` - Directory to index (defaults to current directory)
|
|
48
|
+
|
|
49
|
+
**Features:**
|
|
50
|
+
- Incremental indexing (only re-indexes changed files)
|
|
51
|
+
- Respects `.gitignore` patterns
|
|
52
|
+
- Supports multiple languages (TypeScript, JavaScript, Python, Rust, Go, Java, C/C++, and more)
|
|
53
|
+
|
|
54
|
+
### `optiq watch [options]`
|
|
55
|
+
Watch your codebase for changes and automatically re-index modified files.
|
|
56
|
+
|
|
57
|
+
**Options:**
|
|
58
|
+
- `--path <directory>` - Directory to watch (defaults to current directory)
|
|
59
|
+
|
|
60
|
+
**Features:**
|
|
61
|
+
- Real-time file monitoring
|
|
62
|
+
- Debounced indexing (waits 5 seconds after last change)
|
|
63
|
+
- Automatic incremental updates
|
|
64
|
+
|
|
65
|
+
## Use Cases
|
|
66
|
+
|
|
67
|
+
- **AI-Powered Code Search**: Use with the Optiq MCP server for intelligent codebase queries
|
|
68
|
+
- **Context Engine**: Provide relevant code context to AI assistants
|
|
69
|
+
- **Documentation**: Keep your code index up-to-date for documentation tools
|
|
70
|
+
- **Code Analysis**: Enable advanced code analysis and insights
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
The CLI stores your authentication token in `~/.optiq/config.json`.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Node.js >= 18.0.0
|
|
79
|
+
- An Optiq account (sign up at [optiqcode.com](https://optiqcode.com))
|
|
80
|
+
|
|
81
|
+
## Support
|
|
82
|
+
|
|
83
|
+
- Documentation: [docs.optiqcode.com](https://docs.optiqcode.com)
|
|
84
|
+
- Issues: [github.com/optiqcode/optiq/issues](https://github.com/optiqcode/optiq/issues)
|
|
85
|
+
- Email: support@optiqcode.com
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { getConfig, saveConfig, clearConfig } from '../utils/config.js';
|
|
6
|
+
const API_URL = 'https://optiqcode.com/api';
|
|
7
|
+
export async function login() {
|
|
8
|
+
// Test comment: verifying incremental indexing works correctly
|
|
9
|
+
console.log(chalk.blue('🔐 Login to Optiq\n'));
|
|
10
|
+
const response = await prompts([
|
|
11
|
+
{
|
|
12
|
+
type: 'text',
|
|
13
|
+
name: 'email',
|
|
14
|
+
message: 'Email:',
|
|
15
|
+
validate: (value) => value.includes('@') || 'Please enter a valid email',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'password',
|
|
19
|
+
name: 'password',
|
|
20
|
+
message: 'Password:',
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
if (!response.email || !response.password) {
|
|
24
|
+
console.log(chalk.yellow('\n⚠️ Login cancelled'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const spinner = ora('Logging in...').start();
|
|
28
|
+
try {
|
|
29
|
+
const result = await axios.post(`${API_URL}/auth/login`, {
|
|
30
|
+
email: response.email,
|
|
31
|
+
password: response.password,
|
|
32
|
+
});
|
|
33
|
+
if (result.data.success) {
|
|
34
|
+
await saveConfig({
|
|
35
|
+
email: response.email,
|
|
36
|
+
token: result.data.token,
|
|
37
|
+
apiKey: result.data.context_engine_api_key,
|
|
38
|
+
});
|
|
39
|
+
spinner.succeed(chalk.green(`✓ Logged in as ${response.email}`));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
spinner.fail(chalk.red('✗ Login failed'));
|
|
43
|
+
console.log(chalk.red(result.data.error || 'Unknown error'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
spinner.fail(chalk.red('✗ Login failed'));
|
|
48
|
+
if (error.response?.data?.error) {
|
|
49
|
+
console.log(chalk.red(error.response.data.error));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.red(error.message));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function logout() {
|
|
57
|
+
const config = await getConfig();
|
|
58
|
+
if (!config) {
|
|
59
|
+
console.log(chalk.yellow('⚠️ Not logged in'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await clearConfig();
|
|
63
|
+
console.log(chalk.green('✓ Logged out successfully'));
|
|
64
|
+
}
|
|
65
|
+
export async function whoami() {
|
|
66
|
+
const config = await getConfig();
|
|
67
|
+
if (!config) {
|
|
68
|
+
console.log(chalk.yellow('⚠️ Not logged in'));
|
|
69
|
+
console.log(chalk.dim('Run `optiq login` to get started'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.blue('📧 Logged in as:'), chalk.bold(config.email));
|
|
73
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import { getConfig } from '../utils/config.js';
|
|
8
|
+
import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from '../utils/files.js';
|
|
9
|
+
const API_URL = 'https://optiqcode.com/api';
|
|
10
|
+
export async function index(options) {
|
|
11
|
+
const config = await getConfig();
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.log(chalk.red('✗ Not logged in'));
|
|
14
|
+
console.log(chalk.dim('Run `optiq login` first'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const targetPath = path.resolve(options.path || process.cwd());
|
|
18
|
+
// Safety check: prevent indexing home directory
|
|
19
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
20
|
+
if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
|
|
21
|
+
console.log(chalk.red('✗ Cannot index home directory'));
|
|
22
|
+
console.log(chalk.dim('Please specify a project directory'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Validate directory
|
|
26
|
+
const validation = await isValidDirectory(targetPath);
|
|
27
|
+
if (!validation.valid) {
|
|
28
|
+
console.log(chalk.red(`✗ ${validation.error}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk.blue('📁 Directory:'), chalk.bold(targetPath));
|
|
32
|
+
console.log(chalk.dim(` ${validation.fileCount} files found\n`));
|
|
33
|
+
// Ask for confirmation
|
|
34
|
+
const { confirm } = await prompts({
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'confirm',
|
|
37
|
+
message: 'Index this directory?',
|
|
38
|
+
initial: true,
|
|
39
|
+
});
|
|
40
|
+
if (!confirm) {
|
|
41
|
+
console.log(chalk.yellow('\n⚠️ Indexing cancelled'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const spinner = ora('Collecting files...').start();
|
|
45
|
+
try {
|
|
46
|
+
// Collect all files
|
|
47
|
+
const files = await collectFiles(targetPath);
|
|
48
|
+
spinner.text = `Reading ${files.length} files...`;
|
|
49
|
+
// Read file contents
|
|
50
|
+
const fileContents = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
54
|
+
const relativePath = path.relative(targetPath, file);
|
|
55
|
+
// CRITICAL FIX: Normalize path separators to forward slashes for cross-platform consistency
|
|
56
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
57
|
+
fileContents.push({
|
|
58
|
+
path: normalizedPath,
|
|
59
|
+
content: content,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Skip files that can't be read
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
spinner.text = 'Uploading to Optiq...';
|
|
67
|
+
// Send to API
|
|
68
|
+
const response = await axios.post(`${API_URL}/nexus/index/content`, {
|
|
69
|
+
repository_path: targetPath,
|
|
70
|
+
files: fileContents,
|
|
71
|
+
}, {
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
timeout: 120000,
|
|
77
|
+
});
|
|
78
|
+
if (response.data.success) {
|
|
79
|
+
spinner.succeed(chalk.green('✓ Indexing complete'));
|
|
80
|
+
console.log(chalk.blue('📊 Repository ID:'), chalk.bold(response.data.repo_id));
|
|
81
|
+
console.log(chalk.blue('📁 Files indexed:'), chalk.bold(response.data.files_processed));
|
|
82
|
+
console.log(chalk.blue('📝 Entities indexed:'), chalk.bold(response.data.entities_indexed));
|
|
83
|
+
console.log(chalk.dim('\nUse this repo_id with the MCP server or API'));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
spinner.fail(chalk.red('✗ Indexing failed'));
|
|
87
|
+
console.log(chalk.red(response.data.error || 'Unknown error'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
spinner.fail(chalk.red('✗ Indexing failed'));
|
|
92
|
+
if (error.response?.data?.error) {
|
|
93
|
+
console.log(chalk.red(error.response.data.error));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(chalk.red(error.message));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function collectFiles(dir) {
|
|
101
|
+
const files = [];
|
|
102
|
+
const ignorePatterns = await getGitIgnorePatterns(dir);
|
|
103
|
+
async function walk(currentPath) {
|
|
104
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
107
|
+
const relativePath = path.relative(dir, fullPath);
|
|
108
|
+
if (shouldIgnoreFile(relativePath, ignorePatterns)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
await walk(fullPath);
|
|
113
|
+
}
|
|
114
|
+
else if (entry.isFile()) {
|
|
115
|
+
files.push(fullPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
await walk(dir);
|
|
120
|
+
return files;
|
|
121
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import { getConfig } from '../utils/config.js';
|
|
8
|
+
import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from '../utils/files.js';
|
|
9
|
+
const API_URL = 'https://optiqcode.com/api';
|
|
10
|
+
export async function watch(options) {
|
|
11
|
+
const config = await getConfig();
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.log(chalk.red('✗ Not logged in'));
|
|
14
|
+
console.log(chalk.dim('Run `optiq login` first'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const targetPath = path.resolve(options.path || process.cwd());
|
|
18
|
+
// Safety check: prevent watching home directory
|
|
19
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
20
|
+
if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
|
|
21
|
+
console.log(chalk.red('✗ Cannot watch home directory'));
|
|
22
|
+
console.log(chalk.dim('Please specify a project directory'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Validate directory
|
|
26
|
+
const validation = await isValidDirectory(targetPath);
|
|
27
|
+
if (!validation.valid) {
|
|
28
|
+
console.log(chalk.red(`✗ ${validation.error}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk.blue('📁 Directory:'), chalk.bold(targetPath));
|
|
32
|
+
console.log(chalk.dim(` ${validation.fileCount} files found\n`));
|
|
33
|
+
// Ask for confirmation
|
|
34
|
+
const { confirm } = await prompts({
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'confirm',
|
|
37
|
+
message: 'Start watching this directory?',
|
|
38
|
+
initial: true,
|
|
39
|
+
});
|
|
40
|
+
if (!confirm) {
|
|
41
|
+
console.log(chalk.yellow('\n⚠️ Watch cancelled'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log(chalk.green('✓ Starting file watcher...\n'));
|
|
45
|
+
const ignorePatterns = await getGitIgnorePatterns(targetPath);
|
|
46
|
+
// Create watcher
|
|
47
|
+
const watcher = chokidar.watch(targetPath, {
|
|
48
|
+
ignored: (filePath) => {
|
|
49
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
50
|
+
return shouldIgnoreFile(relativePath, ignorePatterns);
|
|
51
|
+
},
|
|
52
|
+
persistent: true,
|
|
53
|
+
ignoreInitial: true,
|
|
54
|
+
awaitWriteFinish: {
|
|
55
|
+
stabilityThreshold: 2000,
|
|
56
|
+
pollInterval: 100,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
let repoId = null;
|
|
60
|
+
const pendingChanges = new Map();
|
|
61
|
+
let debounceTimer = null;
|
|
62
|
+
const processChanges = async () => {
|
|
63
|
+
if (pendingChanges.size === 0)
|
|
64
|
+
return;
|
|
65
|
+
const changes = Array.from(pendingChanges.entries());
|
|
66
|
+
pendingChanges.clear();
|
|
67
|
+
console.log(chalk.blue(`\n📝 Processing ${changes.length} changes...`));
|
|
68
|
+
try {
|
|
69
|
+
const fileContents = [];
|
|
70
|
+
for (const [filePath, changeType] of changes) {
|
|
71
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
72
|
+
// CRITICAL FIX: Normalize path separators to forward slashes for cross-platform consistency
|
|
73
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
74
|
+
if (changeType === 'unlink') {
|
|
75
|
+
fileContents.push({
|
|
76
|
+
path: normalizedPath,
|
|
77
|
+
content: null,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
try {
|
|
82
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
83
|
+
fileContents.push({
|
|
84
|
+
path: normalizedPath,
|
|
85
|
+
content: content,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// Skip files that can't be read
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const response = await axios.post(`${API_URL}/nexus/index/incremental`, {
|
|
94
|
+
repository_id: repoId,
|
|
95
|
+
repository_path: targetPath,
|
|
96
|
+
files: fileContents,
|
|
97
|
+
}, {
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
timeout: 60000,
|
|
103
|
+
});
|
|
104
|
+
if (response.data.success) {
|
|
105
|
+
if (!repoId) {
|
|
106
|
+
repoId = response.data.repo_id || response.data.repository_id;
|
|
107
|
+
console.log(chalk.blue('📊 Repository ID:'), chalk.bold(repoId));
|
|
108
|
+
}
|
|
109
|
+
console.log(chalk.green(`✓ Indexed ${response.data.files_processed || changes.length} files, ${response.data.entities_indexed || 0} entities`));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(chalk.red('✗ Indexing failed:'), response.data.error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.log(chalk.red('✗ Indexing failed:'), error.message);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const scheduleProcess = () => {
|
|
120
|
+
if (debounceTimer) {
|
|
121
|
+
clearTimeout(debounceTimer);
|
|
122
|
+
}
|
|
123
|
+
// Wait 5 seconds after last change before indexing to avoid spam
|
|
124
|
+
debounceTimer = setTimeout(processChanges, 5000);
|
|
125
|
+
};
|
|
126
|
+
watcher
|
|
127
|
+
.on('add', (filePath) => {
|
|
128
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
129
|
+
console.log(chalk.dim(` + ${relativePath}`));
|
|
130
|
+
pendingChanges.set(filePath, 'add');
|
|
131
|
+
scheduleProcess();
|
|
132
|
+
})
|
|
133
|
+
.on('change', (filePath) => {
|
|
134
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
135
|
+
console.log(chalk.dim(` ~ ${relativePath}`));
|
|
136
|
+
pendingChanges.set(filePath, 'change');
|
|
137
|
+
scheduleProcess();
|
|
138
|
+
})
|
|
139
|
+
.on('unlink', (filePath) => {
|
|
140
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
141
|
+
console.log(chalk.dim(` - ${relativePath}`));
|
|
142
|
+
pendingChanges.set(filePath, 'unlink');
|
|
143
|
+
scheduleProcess();
|
|
144
|
+
})
|
|
145
|
+
.on('error', (error) => {
|
|
146
|
+
console.log(chalk.red('✗ Watcher error:'), error.message);
|
|
147
|
+
});
|
|
148
|
+
console.log(chalk.green('👀 Watching for changes...'));
|
|
149
|
+
console.log(chalk.dim('Press Ctrl+C to stop\n'));
|
|
150
|
+
// Keep process alive
|
|
151
|
+
process.on('SIGINT', () => {
|
|
152
|
+
console.log(chalk.yellow('\n\n⚠️ Stopping watcher...'));
|
|
153
|
+
watcher.close();
|
|
154
|
+
process.exit(0);
|
|
155
|
+
});
|
|
156
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import logUpdate from 'log-update';
|
|
10
|
+
import { getConfig, saveConfig } from './utils/config.js';
|
|
11
|
+
import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from './utils/files.js';
|
|
12
|
+
const BACKEND_URL = process.env.OPTIQ_BACKEND_URL || 'http://localhost:3002';
|
|
13
|
+
async function showBanner() {
|
|
14
|
+
console.clear();
|
|
15
|
+
console.log(chalk.white.bold(`
|
|
16
|
+
___ _ _
|
|
17
|
+
/ _ \\ _ __ | |_(_) __ _
|
|
18
|
+
| | | | '_ \\| __| |/ _\` |
|
|
19
|
+
| |_| | |_) | |_| | (_| |
|
|
20
|
+
\\___/| .__/ \\__|_|\\__, |
|
|
21
|
+
|_| |_|
|
|
22
|
+
`));
|
|
23
|
+
console.log(chalk.gray(' Automatic code indexing\n'));
|
|
24
|
+
}
|
|
25
|
+
async function login() {
|
|
26
|
+
console.log(chalk.white('🔐 Passwordless login\n'));
|
|
27
|
+
const { email } = await prompts({
|
|
28
|
+
type: 'text',
|
|
29
|
+
name: 'email',
|
|
30
|
+
message: 'Email:',
|
|
31
|
+
validate: (value) => value.includes('@') || 'Please enter a valid email',
|
|
32
|
+
});
|
|
33
|
+
if (!email) {
|
|
34
|
+
console.log(chalk.gray('\n⚠️ Login cancelled'));
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
const spinner = ora({ text: 'Sending code...', color: 'white' }).start();
|
|
38
|
+
try {
|
|
39
|
+
// Send OTP
|
|
40
|
+
const otpResponse = await axios.post(`${BACKEND_URL}/api/auth/send-otp`, { email }, { timeout: 10000 });
|
|
41
|
+
if (!otpResponse.data.success) {
|
|
42
|
+
throw new Error('Failed to send code');
|
|
43
|
+
}
|
|
44
|
+
spinner.succeed(chalk.white('✓ Code sent to your email\n'));
|
|
45
|
+
const { code } = await prompts({
|
|
46
|
+
type: 'text',
|
|
47
|
+
name: 'code',
|
|
48
|
+
message: 'Enter code:',
|
|
49
|
+
validate: (value) => {
|
|
50
|
+
const cleaned = value.replace(/\s/g, '');
|
|
51
|
+
return cleaned.length >= 5 || 'Invalid code';
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (!code) {
|
|
55
|
+
console.log(chalk.gray('\n⚠️ Login cancelled'));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const verifySpinner = ora({ text: 'Verifying...', color: 'white' }).start();
|
|
59
|
+
// Verify OTP
|
|
60
|
+
const verifyResponse = await axios.post(`${BACKEND_URL}/api/auth/verify-otp`, { email, code }, { timeout: 10000 });
|
|
61
|
+
if (verifyResponse.data.success) {
|
|
62
|
+
// Get token from response body or cookie
|
|
63
|
+
let token = verifyResponse.data.auth_token;
|
|
64
|
+
// If no token in body, extract from cookie
|
|
65
|
+
if (!token || token === '') {
|
|
66
|
+
const cookies = verifyResponse.headers['set-cookie'];
|
|
67
|
+
if (cookies) {
|
|
68
|
+
const authCookie = cookies.find((c) => c.startsWith('auth_token='));
|
|
69
|
+
if (authCookie) {
|
|
70
|
+
token = authCookie.split(';')[0].split('=')[1];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!token) {
|
|
75
|
+
verifySpinner.fail(chalk.gray('✗ No token received'));
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// Get API key by validating the token
|
|
79
|
+
const validateResponse = await axios.post(`${BACKEND_URL}/api/auth/validate`, { token }, { timeout: 5000 });
|
|
80
|
+
await saveConfig({
|
|
81
|
+
email: email,
|
|
82
|
+
token: token,
|
|
83
|
+
apiKey: validateResponse.data.context_engine_api_key || '',
|
|
84
|
+
});
|
|
85
|
+
verifySpinner.succeed(chalk.white(`✓ Logged in as ${email}\n`));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
verifySpinner.fail(chalk.gray('✗ Invalid code'));
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
spinner.fail(chalk.gray('✗ Login failed'));
|
|
95
|
+
if (error.response?.data?.error) {
|
|
96
|
+
console.log(chalk.gray(error.response.data.error));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(chalk.gray(error.message));
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function main() {
|
|
105
|
+
await showBanner();
|
|
106
|
+
// Check if logged in
|
|
107
|
+
let config = await getConfig();
|
|
108
|
+
if (!config) {
|
|
109
|
+
const success = await login();
|
|
110
|
+
if (!success) {
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
config = await getConfig();
|
|
114
|
+
if (!config) {
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(chalk.white('✓ Logged in as'), chalk.white.bold(config.email));
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
const targetPath = path.resolve(process.cwd());
|
|
123
|
+
// Safety check: prevent indexing home directory
|
|
124
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
125
|
+
if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
|
|
126
|
+
console.log(chalk.gray('✗ Cannot index home directory'));
|
|
127
|
+
console.log(chalk.gray('Please run this from a project directory'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
// Validate directory
|
|
131
|
+
const validation = await isValidDirectory(targetPath);
|
|
132
|
+
if (!validation.valid) {
|
|
133
|
+
console.log(chalk.gray(`✗ ${validation.error}`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
console.log(chalk.white('📁 Directory:'), chalk.white.bold(path.basename(targetPath)));
|
|
137
|
+
console.log(chalk.gray(` ${validation.fileCount} files found`));
|
|
138
|
+
console.log(chalk.gray(` ${targetPath}\n`));
|
|
139
|
+
// Ask what to do
|
|
140
|
+
const { action } = await prompts({
|
|
141
|
+
type: 'select',
|
|
142
|
+
name: 'action',
|
|
143
|
+
message: 'What would you like to do?',
|
|
144
|
+
choices: [
|
|
145
|
+
{ title: '👀 Watch and auto-index changes', value: 'watch' },
|
|
146
|
+
{ title: '📦 Index once', value: 'index' },
|
|
147
|
+
{ title: '🚪 Logout', value: 'logout' },
|
|
148
|
+
{ title: '❌ Exit', value: 'exit' },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
if (action === 'exit') {
|
|
152
|
+
console.log(chalk.gray('\nGoodbye!'));
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
if (action === 'logout') {
|
|
156
|
+
const { clearConfig } = await import('./utils/config.js');
|
|
157
|
+
await clearConfig();
|
|
158
|
+
console.log(chalk.white('\n✓ Logged out successfully'));
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
if (action === 'index') {
|
|
162
|
+
await indexOnce(targetPath, config);
|
|
163
|
+
}
|
|
164
|
+
else if (action === 'watch') {
|
|
165
|
+
await watchDirectory(targetPath, config);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function indexOnce(targetPath, config) {
|
|
169
|
+
const spinner = ora({ text: 'Collecting files...', color: 'white' }).start();
|
|
170
|
+
try {
|
|
171
|
+
const files = await collectFiles(targetPath);
|
|
172
|
+
spinner.text = `Reading ${files.length} files...`;
|
|
173
|
+
const fileContents = {};
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
try {
|
|
176
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
177
|
+
const relativePath = path.relative(targetPath, file);
|
|
178
|
+
fileContents[relativePath] = content;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
// Skip files that can't be read
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
spinner.text = 'Uploading to Optiq...';
|
|
185
|
+
const filesArray = Object.entries(fileContents).map(([path, content]) => ({
|
|
186
|
+
path,
|
|
187
|
+
content,
|
|
188
|
+
}));
|
|
189
|
+
const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
|
|
190
|
+
repository_path: targetPath,
|
|
191
|
+
files: filesArray,
|
|
192
|
+
}, {
|
|
193
|
+
headers: {
|
|
194
|
+
'X-API-Key': config.apiKey,
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
timeout: 120000,
|
|
198
|
+
});
|
|
199
|
+
if (response.data.success) {
|
|
200
|
+
spinner.succeed(chalk.white('✓ Indexing complete'));
|
|
201
|
+
console.log(chalk.white('\n📊 Repository ID:'), chalk.white.bold(response.data.repo_id));
|
|
202
|
+
console.log(chalk.gray(' Use this with the MCP server or API\n'));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
spinner.fail(chalk.gray('✗ Indexing failed'));
|
|
206
|
+
console.log(chalk.gray(response.data.error || 'Unknown error'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
spinner.fail(chalk.gray('✗ Indexing failed'));
|
|
211
|
+
if (error.response?.data?.error) {
|
|
212
|
+
console.log(chalk.gray(error.response.data.error));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(chalk.gray(error.message));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function watchDirectory(targetPath, config) {
|
|
220
|
+
// Check if repo is already indexed
|
|
221
|
+
let repoId = null;
|
|
222
|
+
try {
|
|
223
|
+
const repoListResponse = await axios.get(`${BACKEND_URL}/api/repositories`, {
|
|
224
|
+
headers: {
|
|
225
|
+
'X-API-Key': config.apiKey,
|
|
226
|
+
},
|
|
227
|
+
timeout: 10000,
|
|
228
|
+
});
|
|
229
|
+
if (repoListResponse.data.success && repoListResponse.data.repositories) {
|
|
230
|
+
const existingRepo = repoListResponse.data.repositories.find((r) => r.path === targetPath);
|
|
231
|
+
if (existingRepo) {
|
|
232
|
+
repoId = existingRepo.id;
|
|
233
|
+
console.log(chalk.white('✓ Repository already indexed'));
|
|
234
|
+
console.log(chalk.white('📊 Repository ID:'), chalk.white.bold(repoId));
|
|
235
|
+
console.log();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Ignore errors, will do full index
|
|
241
|
+
}
|
|
242
|
+
// Only do full index if repo doesn't exist
|
|
243
|
+
if (!repoId) {
|
|
244
|
+
const spinner = ora({ text: 'Collecting files...', color: 'white' }).start();
|
|
245
|
+
try {
|
|
246
|
+
const files = await collectFiles(targetPath);
|
|
247
|
+
spinner.text = `Indexing ${files.length} files...`;
|
|
248
|
+
const fileContents = {};
|
|
249
|
+
let processed = 0;
|
|
250
|
+
for (const file of files) {
|
|
251
|
+
try {
|
|
252
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
253
|
+
const relativePath = path.relative(targetPath, file);
|
|
254
|
+
fileContents[relativePath] = content;
|
|
255
|
+
processed++;
|
|
256
|
+
// Update progress
|
|
257
|
+
const percent = Math.round((processed / files.length) * 100);
|
|
258
|
+
spinner.text = `Indexing ${processed}/${files.length} files (${percent}%)`;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
// Skip files that can't be read
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
spinner.text = 'Uploading to Optiq...';
|
|
265
|
+
const filesArray = Object.entries(fileContents).map(([path, content]) => ({
|
|
266
|
+
path,
|
|
267
|
+
content,
|
|
268
|
+
}));
|
|
269
|
+
const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
|
|
270
|
+
repository_path: targetPath,
|
|
271
|
+
files: filesArray,
|
|
272
|
+
}, {
|
|
273
|
+
headers: {
|
|
274
|
+
'X-API-Key': config.apiKey,
|
|
275
|
+
'Content-Type': 'application/json',
|
|
276
|
+
},
|
|
277
|
+
timeout: 120000,
|
|
278
|
+
});
|
|
279
|
+
if (response.data.success) {
|
|
280
|
+
repoId = response.data.repo_id;
|
|
281
|
+
spinner.succeed(chalk.white(`Indexed ${files.length} files`));
|
|
282
|
+
console.log(chalk.gray('Repository ID:'), chalk.white.bold(repoId));
|
|
283
|
+
console.log();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
spinner.fail(chalk.gray('Initial indexing failed'));
|
|
287
|
+
console.log(chalk.gray(response.data.error || 'Unknown error'));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
spinner.fail(chalk.gray('✗ Initial indexing failed'));
|
|
293
|
+
if (error.response?.data?.error) {
|
|
294
|
+
console.log(chalk.gray(error.response.data.error));
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
console.log(chalk.gray(error.message));
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
console.log(chalk.white('👀 Watching for changes...\n'));
|
|
303
|
+
const ignorePatterns = await getGitIgnorePatterns(targetPath);
|
|
304
|
+
const watcher = chokidar.watch(targetPath, {
|
|
305
|
+
ignored: (filePath) => {
|
|
306
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
307
|
+
return shouldIgnoreFile(relativePath, ignorePatterns);
|
|
308
|
+
},
|
|
309
|
+
persistent: true,
|
|
310
|
+
ignoreInitial: true,
|
|
311
|
+
awaitWriteFinish: {
|
|
312
|
+
stabilityThreshold: 300,
|
|
313
|
+
pollInterval: 50,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const pendingChanges = new Map();
|
|
317
|
+
let debounceTimer = null;
|
|
318
|
+
const allIndexedFiles = new Set();
|
|
319
|
+
const fileContentCache = new Map();
|
|
320
|
+
let isProcessing = false;
|
|
321
|
+
let totalIndexed = 0;
|
|
322
|
+
let sessionStartTime = Date.now();
|
|
323
|
+
let lastIndexedFile = '';
|
|
324
|
+
let lastIndexedTime = Date.now();
|
|
325
|
+
const formatUptime = (ms) => {
|
|
326
|
+
const seconds = Math.floor(ms / 1000);
|
|
327
|
+
const minutes = Math.floor(seconds / 60);
|
|
328
|
+
const hours = Math.floor(minutes / 60);
|
|
329
|
+
if (hours > 0)
|
|
330
|
+
return `${hours}h ${minutes % 60}m`;
|
|
331
|
+
if (minutes > 0)
|
|
332
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
333
|
+
return `${seconds}s`;
|
|
334
|
+
};
|
|
335
|
+
const updateDashboard = () => {
|
|
336
|
+
const uptime = formatUptime(Date.now() - sessionStartTime);
|
|
337
|
+
const timeSinceLastIndex = Math.floor((Date.now() - lastIndexedTime) / 1000);
|
|
338
|
+
const lines = [];
|
|
339
|
+
lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
340
|
+
lines.push(`${chalk.cyan('📊 Session Stats')}`);
|
|
341
|
+
lines.push(` ${chalk.gray('Uptime:')} ${chalk.white(uptime)}`);
|
|
342
|
+
lines.push(` ${chalk.gray('Total Indexed:')} ${chalk.white(totalIndexed)} ${chalk.gray('changes')}`);
|
|
343
|
+
lines.push(` ${chalk.gray('Unique Files:')} ${chalk.white(allIndexedFiles.size)}`);
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push(`${chalk.green('●')} ${chalk.gray('Last Activity:')} ${chalk.white(lastIndexedFile || 'None')}`);
|
|
346
|
+
if (lastIndexedFile) {
|
|
347
|
+
lines.push(` ${chalk.gray(`${timeSinceLastIndex}s ago`)}`);
|
|
348
|
+
}
|
|
349
|
+
lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
350
|
+
lines.push('');
|
|
351
|
+
logUpdate(lines.join('\n'));
|
|
352
|
+
};
|
|
353
|
+
// Update dashboard every 5 seconds to show uptime
|
|
354
|
+
const dashboardInterval = setInterval(updateDashboard, 5000);
|
|
355
|
+
const processChanges = async () => {
|
|
356
|
+
if (pendingChanges.size === 0 || isProcessing)
|
|
357
|
+
return;
|
|
358
|
+
isProcessing = true;
|
|
359
|
+
const changes = Array.from(pendingChanges.entries());
|
|
360
|
+
pendingChanges.clear();
|
|
361
|
+
// Collect unique files first
|
|
362
|
+
const uniqueFiles = new Set();
|
|
363
|
+
for (const [filePath] of changes) {
|
|
364
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
365
|
+
uniqueFiles.add(relativePath);
|
|
366
|
+
allIndexedFiles.add(relativePath);
|
|
367
|
+
}
|
|
368
|
+
const fileList = Array.from(uniqueFiles);
|
|
369
|
+
try {
|
|
370
|
+
const filesArray = [];
|
|
371
|
+
let hasChanges = false;
|
|
372
|
+
for (const [filePath, changeType] of changes) {
|
|
373
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
374
|
+
if (changeType === 'unlink') {
|
|
375
|
+
filesArray.push({ path: relativePath, content: null });
|
|
376
|
+
fileContentCache.delete(relativePath);
|
|
377
|
+
hasChanges = true;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
try {
|
|
381
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
382
|
+
// Skip if content hasn't changed
|
|
383
|
+
if (fileContentCache.get(relativePath) === content) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
fileContentCache.set(relativePath, content);
|
|
387
|
+
filesArray.push({ path: relativePath, content });
|
|
388
|
+
hasChanges = true;
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
// Skip files that can't be read
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// If no actual changes, skip indexing
|
|
396
|
+
if (!hasChanges || filesArray.length === 0) {
|
|
397
|
+
isProcessing = false;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const response = await axios.post(`${BACKEND_URL}/api/nexus/index/incremental`, {
|
|
401
|
+
repository_id: repoId,
|
|
402
|
+
repository_path: targetPath,
|
|
403
|
+
files: filesArray,
|
|
404
|
+
}, {
|
|
405
|
+
headers: {
|
|
406
|
+
'X-API-Key': config.apiKey,
|
|
407
|
+
'Content-Type': 'application/json',
|
|
408
|
+
},
|
|
409
|
+
timeout: 60000,
|
|
410
|
+
});
|
|
411
|
+
if (response.data.success) {
|
|
412
|
+
totalIndexed += filesArray.length;
|
|
413
|
+
lastIndexedTime = Date.now();
|
|
414
|
+
// Update last indexed file
|
|
415
|
+
if (filesArray.length === 1) {
|
|
416
|
+
lastIndexedFile = filesArray[0].path;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
lastIndexedFile = `${filesArray.length} files`;
|
|
420
|
+
}
|
|
421
|
+
// Update dashboard
|
|
422
|
+
updateDashboard();
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
logUpdate.clear();
|
|
426
|
+
console.log(chalk.gray(`✗ Indexing failed`));
|
|
427
|
+
console.log(chalk.gray(response.data.error));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
logUpdate.clear();
|
|
432
|
+
console.log(chalk.gray(`✗ Indexing failed`));
|
|
433
|
+
console.log(chalk.gray(error.response?.data || error.message));
|
|
434
|
+
}
|
|
435
|
+
isProcessing = false;
|
|
436
|
+
// Check if there are more pending changes and process them
|
|
437
|
+
if (pendingChanges.size > 0) {
|
|
438
|
+
scheduleProcess();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const scheduleProcess = () => {
|
|
442
|
+
if (debounceTimer) {
|
|
443
|
+
clearTimeout(debounceTimer);
|
|
444
|
+
}
|
|
445
|
+
debounceTimer = setTimeout(processChanges, 50); // Batch rapid changes
|
|
446
|
+
};
|
|
447
|
+
watcher
|
|
448
|
+
.on('add', (filePath) => {
|
|
449
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
450
|
+
pendingChanges.set(filePath, 'add');
|
|
451
|
+
scheduleProcess();
|
|
452
|
+
})
|
|
453
|
+
.on('change', (filePath) => {
|
|
454
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
455
|
+
pendingChanges.set(filePath, 'change');
|
|
456
|
+
scheduleProcess();
|
|
457
|
+
})
|
|
458
|
+
.on('unlink', (filePath) => {
|
|
459
|
+
const relativePath = path.relative(targetPath, filePath);
|
|
460
|
+
pendingChanges.set(filePath, 'unlink');
|
|
461
|
+
scheduleProcess();
|
|
462
|
+
})
|
|
463
|
+
.on('error', (error) => {
|
|
464
|
+
console.log(chalk.gray('✗ Watcher error:'), error.message);
|
|
465
|
+
});
|
|
466
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
467
|
+
process.on('SIGINT', () => {
|
|
468
|
+
console.log(chalk.gray('\n\n⚠️ Stopping watcher...'));
|
|
469
|
+
watcher.close();
|
|
470
|
+
process.exit(0);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async function collectFiles(dir) {
|
|
474
|
+
const files = [];
|
|
475
|
+
const ignorePatterns = await getGitIgnorePatterns(dir);
|
|
476
|
+
async function walk(currentPath) {
|
|
477
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
478
|
+
for (const entry of entries) {
|
|
479
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
480
|
+
const relativePath = path.relative(dir, fullPath);
|
|
481
|
+
if (shouldIgnoreFile(relativePath, ignorePatterns)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (entry.isDirectory()) {
|
|
485
|
+
await walk(fullPath);
|
|
486
|
+
}
|
|
487
|
+
else if (entry.isFile()) {
|
|
488
|
+
files.push(fullPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
await walk(dir);
|
|
493
|
+
return files;
|
|
494
|
+
}
|
|
495
|
+
main().catch((error) => {
|
|
496
|
+
console.error(chalk.gray('\n✗ Fatal error:'), error.message);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
email: string;
|
|
3
|
+
token: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
expiryTime?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function getConfig(): Promise<Config | null>;
|
|
8
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
9
|
+
export declare function clearConfig(): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.optiq');
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'session.json');
|
|
6
|
+
export async function getConfig() {
|
|
7
|
+
try {
|
|
8
|
+
const data = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
9
|
+
const config = JSON.parse(data);
|
|
10
|
+
// Check if token is expired
|
|
11
|
+
if (config.expiryTime && Date.now() > config.expiryTime) {
|
|
12
|
+
await clearConfig();
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
email: config.email,
|
|
17
|
+
token: config.token,
|
|
18
|
+
apiKey: config.contextEngineApiKey || config.apiKey,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function saveConfig(config) {
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
// Calculate expiry time (30 days from now)
|
|
29
|
+
const expiryTime = Date.now() + (30 * 24 * 60 * 60 * 1000);
|
|
30
|
+
const sessionData = {
|
|
31
|
+
token: config.token,
|
|
32
|
+
email: config.email,
|
|
33
|
+
expiryTime,
|
|
34
|
+
contextEngineApiKey: config.apiKey,
|
|
35
|
+
};
|
|
36
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(sessionData, null, 2), 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new Error('Failed to save config');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function clearConfig() {
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(CONFIG_FILE);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Ignore if file doesn't exist
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isValidDirectory(dirPath: string): Promise<{
|
|
2
|
+
valid: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
fileCount?: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function getGitIgnorePatterns(dir: string): Promise<string[]>;
|
|
7
|
+
export declare function shouldIgnoreFile(relativePath: string, patterns: string[]): boolean;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
4
|
+
'node_modules',
|
|
5
|
+
'.git',
|
|
6
|
+
'dist',
|
|
7
|
+
'build',
|
|
8
|
+
'target',
|
|
9
|
+
'.next',
|
|
10
|
+
'.nuxt',
|
|
11
|
+
'coverage',
|
|
12
|
+
'.cache',
|
|
13
|
+
'.vscode',
|
|
14
|
+
'.idea',
|
|
15
|
+
'*.log',
|
|
16
|
+
'.DS_Store',
|
|
17
|
+
'Thumbs.db',
|
|
18
|
+
];
|
|
19
|
+
export async function isValidDirectory(dirPath) {
|
|
20
|
+
try {
|
|
21
|
+
const stat = await fs.stat(dirPath);
|
|
22
|
+
if (!stat.isDirectory()) {
|
|
23
|
+
return { valid: false, error: 'Path is not a directory' };
|
|
24
|
+
}
|
|
25
|
+
// Count files
|
|
26
|
+
const fileCount = await countFiles(dirPath);
|
|
27
|
+
if (fileCount === 0) {
|
|
28
|
+
return { valid: false, error: 'Directory is empty' };
|
|
29
|
+
}
|
|
30
|
+
if (fileCount > 50000) {
|
|
31
|
+
return { valid: false, error: 'Directory has too many files (max: 50,000)' };
|
|
32
|
+
}
|
|
33
|
+
return { valid: true, fileCount };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return { valid: false, error: 'Directory does not exist or is not accessible' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function countFiles(dir) {
|
|
40
|
+
let count = 0;
|
|
41
|
+
const ignorePatterns = await getGitIgnorePatterns(dir);
|
|
42
|
+
async function walk(currentPath) {
|
|
43
|
+
if (count > 50000)
|
|
44
|
+
return; // Stop counting if too many
|
|
45
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
48
|
+
const relativePath = path.relative(dir, fullPath);
|
|
49
|
+
if (shouldIgnoreFile(relativePath, ignorePatterns)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
await walk(fullPath);
|
|
54
|
+
}
|
|
55
|
+
else if (entry.isFile()) {
|
|
56
|
+
count++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await walk(dir);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Ignore errors during counting
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
export async function getGitIgnorePatterns(dir) {
|
|
69
|
+
const patterns = [...DEFAULT_IGNORE_PATTERNS];
|
|
70
|
+
try {
|
|
71
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
72
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
73
|
+
const lines = content.split('\n')
|
|
74
|
+
.map(line => line.trim())
|
|
75
|
+
.filter(line => line && !line.startsWith('#'));
|
|
76
|
+
patterns.push(...lines);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// No .gitignore file, use defaults
|
|
80
|
+
}
|
|
81
|
+
return patterns;
|
|
82
|
+
}
|
|
83
|
+
export function shouldIgnoreFile(relativePath, patterns) {
|
|
84
|
+
const parts = relativePath.split(path.sep);
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
// Exact match
|
|
87
|
+
if (parts.includes(pattern)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Wildcard match
|
|
91
|
+
if (pattern.includes('*')) {
|
|
92
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
93
|
+
if (regex.test(relativePath)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Extension match
|
|
98
|
+
if (pattern.startsWith('*.') && relativePath.endsWith(pattern.slice(1))) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optiqcode/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for Optiq - automatic code indexing and context engine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"optiq": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"optiq",
|
|
18
|
+
"code-indexing",
|
|
19
|
+
"context-engine",
|
|
20
|
+
"cli",
|
|
21
|
+
"developer-tools"
|
|
22
|
+
],
|
|
23
|
+
"author": "optiqcode",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/optiqcode/optiq"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/optiqcode/optiq#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/optiqcode/optiq/issues"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"axios": "^1.6.0",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"chokidar": "^3.5.3",
|
|
45
|
+
"log-update": "^7.0.1",
|
|
46
|
+
"ora": "^8.0.1",
|
|
47
|
+
"prompts": "^2.4.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"@types/prompts": "^2.4.9",
|
|
52
|
+
"typescript": "^5.3.0"
|
|
53
|
+
}
|
|
54
|
+
}
|