scai 0.1.89 → 0.1.91
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/README.md +8 -0
- package/dist/CHANGELOG.md +6 -1
- package/dist/commands/DeleteIndex.js +50 -0
- package/dist/commands/SwitchCmd.js +8 -11
- package/dist/config.js +45 -54
- package/dist/db/client.js +9 -16
- package/dist/index.js +7 -0
- package/dist/utils/repoKey.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -301,6 +301,14 @@ scai index switch
|
|
|
301
301
|
|
|
302
302
|
Switch active repository (by key or indexDir). Run without input for an interactive list of repositories.
|
|
303
303
|
|
|
304
|
+
#### `scai index delete`
|
|
305
|
+
|
|
306
|
+
Delete a repository from the index (interactive).
|
|
307
|
+
This removes the repository entry from the `config.json` file, but does **not** delete the repository folder on disk.
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
scai index delete
|
|
311
|
+
```
|
|
304
312
|
|
|
305
313
|
---
|
|
306
314
|
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -112,4 +112,9 @@ Type handling with the module pipeline
|
|
|
112
112
|
## 2025-08-16
|
|
113
113
|
|
|
114
114
|
• Update preserveCodeModule to compare blocks correctly and handle duplicate blocks
|
|
115
|
-
• Normalize line endings and detect comments in preserveCodeModule
|
|
115
|
+
• Normalize line endings and detect comments in preserveCodeModule
|
|
116
|
+
|
|
117
|
+
## 2025-08-18
|
|
118
|
+
|
|
119
|
+
• Add interactive delete repository command
|
|
120
|
+
• Refactor DeleteIndex command to handle repository deletion correctly
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// File: src/commands/delete.ts
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import { Config, writeConfig } from '../config.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
export async function runInteractiveDelete() {
|
|
6
|
+
const config = Config.getRaw();
|
|
7
|
+
const keys = Object.keys(config.repos || {});
|
|
8
|
+
if (!keys.length) {
|
|
9
|
+
console.log('⚠️ No repositories configured.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log('\n🗑️ Repositories Available for Deletion:\n');
|
|
13
|
+
keys.forEach((key, i) => {
|
|
14
|
+
const isActive = config.activeRepo === key ? chalk.green('(active)') : '';
|
|
15
|
+
const dir = config.repos[key]?.indexDir ?? '';
|
|
16
|
+
console.log(`${chalk.blue(`${i + 1})`)} ${key} ${isActive}`);
|
|
17
|
+
console.log(` ↳ ${chalk.grey(dir)}`);
|
|
18
|
+
});
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
rl.question('\n👉 Select a repository number to delete: ', (answer) => {
|
|
24
|
+
rl.close();
|
|
25
|
+
const index = parseInt(answer.trim(), 10) - 1;
|
|
26
|
+
if (isNaN(index) || index < 0 || index >= keys.length) {
|
|
27
|
+
console.log('❌ Invalid selection.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const selectedKey = keys[index];
|
|
31
|
+
console.log(`\n⚠️ Deleting repository: ${chalk.red(selectedKey)}\n`);
|
|
32
|
+
// Build an update that uses the null-sentinel for deletion.
|
|
33
|
+
const update = { repos: { [selectedKey]: null } };
|
|
34
|
+
// If deleting the active repo, pick another one (first remaining) or unset.
|
|
35
|
+
if (config.activeRepo === selectedKey) {
|
|
36
|
+
const remainingKeys = keys.filter(k => k !== selectedKey);
|
|
37
|
+
update.activeRepo = remainingKeys[0] || undefined;
|
|
38
|
+
console.log(`ℹ️ Active repo reset to: ${chalk.green(update.activeRepo ?? 'none')}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
writeConfig(update); // this will actually remove the repo key
|
|
42
|
+
console.log(`✅ Repository "${selectedKey}" removed from config.json.`);
|
|
43
|
+
// NOTE: We intentionally do NOT delete the on-disk repo folder here.
|
|
44
|
+
// Only remove data when a --purge flag is explicitly used.
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error('❌ Failed to update config.json:', err instanceof Error ? err.message : err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
// File: src/commands/switch.ts
|
|
2
2
|
import readline from 'readline';
|
|
3
3
|
import { Config, writeConfig } from '../config.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getRepoKeyForPath } from '../utils/normalizePath.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
export function runSwitchCommand(inputPathOrKey) {
|
|
7
7
|
const config = Config.getRaw();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
config.
|
|
12
|
-
|
|
13
|
-
Config.setGitHubToken(config.repos[normalizedInput].githubToken ?? '');
|
|
14
|
-
console.log(`✅ Switched active repo to key: ${normalizedInput}`);
|
|
8
|
+
// Direct key match
|
|
9
|
+
if (config.repos[inputPathOrKey]) {
|
|
10
|
+
config.activeRepo = inputPathOrKey;
|
|
11
|
+
Config.setGitHubToken(config.repos[inputPathOrKey].githubToken ?? '');
|
|
12
|
+
console.log(`✅ Switched active repo to key: ${inputPathOrKey}`);
|
|
15
13
|
}
|
|
16
14
|
else {
|
|
17
|
-
//
|
|
15
|
+
// Path match only if key match failed
|
|
18
16
|
const repoKey = getRepoKeyForPath(inputPathOrKey, config);
|
|
19
17
|
if (!repoKey) {
|
|
20
18
|
console.error(`❌ No repo found matching path or key: "${inputPathOrKey}"`);
|
|
21
19
|
process.exit(1);
|
|
22
20
|
}
|
|
23
21
|
config.activeRepo = repoKey;
|
|
24
|
-
// Update GitHub token
|
|
25
22
|
Config.setGitHubToken(config.repos[repoKey]?.githubToken ?? '');
|
|
26
23
|
console.log(`✅ Switched active repo to path match: ${repoKey}`);
|
|
27
24
|
}
|
|
28
|
-
//
|
|
25
|
+
// Persist change
|
|
29
26
|
writeConfig(config);
|
|
30
27
|
}
|
|
31
28
|
export async function runInteractiveSwitch() {
|
package/dist/config.js
CHANGED
|
@@ -2,8 +2,9 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { CONFIG_PATH, SCAI_HOME, SCAI_REPOS } from './constants.js';
|
|
4
4
|
import { getDbForRepo } from './db/client.js';
|
|
5
|
-
import {
|
|
5
|
+
import { normalizePath } from './utils/normalizePath.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { getHashedRepoKey } from './utils/repoKey.js';
|
|
7
8
|
const defaultConfig = {
|
|
8
9
|
model: 'llama3',
|
|
9
10
|
contextLength: 8192,
|
|
@@ -38,6 +39,14 @@ export function writeConfig(newCfg) {
|
|
|
38
39
|
...(newCfg.repos || {}),
|
|
39
40
|
},
|
|
40
41
|
};
|
|
42
|
+
// Remove repos explicitly set to null
|
|
43
|
+
if (newCfg.repos) {
|
|
44
|
+
for (const [key, value] of Object.entries(newCfg.repos)) {
|
|
45
|
+
if (value === null) {
|
|
46
|
+
delete merged.repos[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
41
50
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
42
51
|
}
|
|
43
52
|
export const Config = {
|
|
@@ -78,54 +87,47 @@ export const Config = {
|
|
|
78
87
|
}
|
|
79
88
|
},
|
|
80
89
|
getIndexDir() {
|
|
81
|
-
const
|
|
82
|
-
const activeRepo =
|
|
83
|
-
if (activeRepo)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
return '';
|
|
90
|
+
const cfg = readConfig();
|
|
91
|
+
const activeRepo = cfg.activeRepo;
|
|
92
|
+
if (!activeRepo)
|
|
93
|
+
return '';
|
|
94
|
+
return cfg.repos[activeRepo]?.indexDir ?? '';
|
|
88
95
|
},
|
|
89
96
|
async setIndexDir(indexDir) {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
this.setActiveRepo(scaiRepoRoot);
|
|
96
|
-
// Call setRepoIndexDir to update the repo's indexDir and other settings
|
|
97
|
-
await this.setRepoIndexDir(scaiRepoRoot, absPath); // Set the indexDir for the repo
|
|
97
|
+
// Normalize the provided index directory
|
|
98
|
+
const normalizedIndexDir = normalizePath(indexDir);
|
|
99
|
+
// Compute a stable repo key
|
|
100
|
+
const repoKey = getHashedRepoKey(normalizedIndexDir);
|
|
101
|
+
const scaiRepoRoot = path.join(SCAI_REPOS, repoKey);
|
|
98
102
|
// Ensure base folders exist
|
|
99
103
|
fs.mkdirSync(scaiRepoRoot, { recursive: true });
|
|
100
|
-
//
|
|
104
|
+
// Set the active repo using the precomputed repoKey
|
|
105
|
+
this.setActiveRepo(repoKey);
|
|
106
|
+
// Update the repo configuration with the normalized indexDir
|
|
107
|
+
await this.setRepoIndexDir(repoKey, normalizedIndexDir);
|
|
108
|
+
// Initialize DB if it does not exist
|
|
101
109
|
const dbPath = path.join(scaiRepoRoot, 'db.sqlite');
|
|
102
110
|
if (!fs.existsSync(dbPath)) {
|
|
103
111
|
console.log(`📦 Database not found. ${chalk.green('Initializing DB')} at ${normalizePath(dbPath)}`);
|
|
104
|
-
getDbForRepo();
|
|
112
|
+
getDbForRepo();
|
|
105
113
|
}
|
|
106
114
|
},
|
|
107
|
-
|
|
108
|
-
* Set both the scaiRepoRoot for the config and the indexDir (the actual repo root path)
|
|
109
|
-
* @param scaiRepoRoot
|
|
110
|
-
* @param indexDir
|
|
111
|
-
*/
|
|
112
|
-
async setRepoIndexDir(scaiRepoRoot, indexDir) {
|
|
113
|
-
const normalizedRepoPath = normalizePath(scaiRepoRoot);
|
|
114
|
-
const normalizedIndexDir = normalizePath(indexDir);
|
|
115
|
+
async setRepoIndexDir(repoKey, indexDir) {
|
|
115
116
|
const cfg = readConfig();
|
|
116
|
-
if (!cfg.repos[
|
|
117
|
-
cfg.repos[
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
indexDir: normalizedIndexDir, // Ensure the indexDir is always normalized
|
|
117
|
+
if (!cfg.repos[repoKey])
|
|
118
|
+
cfg.repos[repoKey] = {};
|
|
119
|
+
cfg.repos[repoKey] = {
|
|
120
|
+
...cfg.repos[repoKey],
|
|
121
|
+
indexDir, // Already normalized
|
|
122
122
|
};
|
|
123
|
-
await writeConfig(cfg);
|
|
124
|
-
console.log(`✅ Repo index directory set for ${
|
|
123
|
+
await writeConfig(cfg);
|
|
124
|
+
console.log(`✅ Repo index directory set for ${repoKey} : ${indexDir}`);
|
|
125
125
|
},
|
|
126
126
|
setActiveRepo(repoKey) {
|
|
127
127
|
const cfg = readConfig();
|
|
128
|
-
cfg.activeRepo =
|
|
128
|
+
cfg.activeRepo = repoKey;
|
|
129
|
+
if (!cfg.repos[repoKey])
|
|
130
|
+
cfg.repos[repoKey] = {};
|
|
129
131
|
writeConfig(cfg);
|
|
130
132
|
console.log(`✅ Active repo switched to: ${repoKey}`);
|
|
131
133
|
},
|
|
@@ -140,38 +142,27 @@ export const Config = {
|
|
|
140
142
|
for (const key of keys) {
|
|
141
143
|
const r = cfg.repos[key];
|
|
142
144
|
const isActive = cfg.activeRepo === key;
|
|
143
|
-
// Use chalk to ensure proper coloring
|
|
144
145
|
const label = isActive
|
|
145
|
-
? chalk.green(`✅ ${key} (active)`)
|
|
146
|
-
: chalk.white(` ${key}`);
|
|
146
|
+
? chalk.green(`✅ ${key} (active)`)
|
|
147
|
+
: chalk.white(` ${key}`);
|
|
147
148
|
console.log(`- ${label}`);
|
|
148
149
|
console.log(` ↳ indexDir: ${r.indexDir}`);
|
|
149
150
|
}
|
|
150
151
|
},
|
|
151
|
-
// Method to get GitHub token for the active repo
|
|
152
152
|
getGitHubToken() {
|
|
153
153
|
const cfg = readConfig();
|
|
154
154
|
const active = cfg.activeRepo;
|
|
155
|
-
if (active)
|
|
156
|
-
|
|
157
|
-
const normalizedActiveRepo = normalizePath(active);
|
|
158
|
-
return cfg.repos[normalizedActiveRepo]?.githubToken || null;
|
|
159
|
-
}
|
|
160
|
-
// If no activeRepo, fall back to the global githubToken field
|
|
155
|
+
if (active)
|
|
156
|
+
return cfg.repos[active]?.githubToken || null;
|
|
161
157
|
return cfg.githubToken || null;
|
|
162
158
|
},
|
|
163
159
|
setGitHubToken(token) {
|
|
164
160
|
const cfg = readConfig();
|
|
165
161
|
const active = cfg.activeRepo;
|
|
166
162
|
if (active) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
cfg.repos[repoKey] = {
|
|
172
|
-
...cfg.repos[repoKey],
|
|
173
|
-
githubToken: token,
|
|
174
|
-
};
|
|
163
|
+
if (!cfg.repos[active])
|
|
164
|
+
cfg.repos[active] = {};
|
|
165
|
+
cfg.repos[active] = { ...cfg.repos[active], githubToken: token };
|
|
175
166
|
}
|
|
176
167
|
else {
|
|
177
168
|
cfg.githubToken = token;
|
|
@@ -184,7 +175,7 @@ export const Config = {
|
|
|
184
175
|
const active = cfg.activeRepo;
|
|
185
176
|
console.log(`🔧 Current configuration:`);
|
|
186
177
|
console.log(` Active index dir: ${active || 'Not Set'}`);
|
|
187
|
-
const repoCfg = active ? cfg.repos
|
|
178
|
+
const repoCfg = active ? cfg.repos[active] : {};
|
|
188
179
|
console.log(` Model : ${repoCfg?.model || cfg.model}`);
|
|
189
180
|
console.log(` Language : ${repoCfg?.language || cfg.language}`);
|
|
190
181
|
console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
|
package/dist/db/client.js
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { SCAI_HOME } from '../constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import { readConfig } from '../config.js';
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
6
|
/**
|
|
7
7
|
* Returns a per-repo SQLite database instance.
|
|
8
8
|
* Ensures the directory and file are created.
|
|
9
9
|
*/
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export function getDbPathForRepo() {
|
|
11
|
+
const cfg = readConfig();
|
|
12
|
+
const repoKey = cfg.activeRepo;
|
|
13
|
+
if (!repoKey) {
|
|
14
|
+
throw new Error('No active repo set. Please set an index directory first.');
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
return path.join(SCAI_HOME, 'repos', repoKey, 'db.sqlite');
|
|
17
|
+
}
|
|
18
|
+
export function getDbForRepo() {
|
|
16
19
|
const dbPath = getDbPathForRepo();
|
|
17
20
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
18
21
|
const db = new Database(dbPath);
|
|
19
22
|
db.pragma('journal_mode = WAL');
|
|
20
23
|
return db;
|
|
21
24
|
}
|
|
22
|
-
export function getDbPathForRepo() {
|
|
23
|
-
const repoRoot = Config.getIndexDir();
|
|
24
|
-
if (!repoRoot) {
|
|
25
|
-
throw new Error('No index directory set. Please set an index directory first.');
|
|
26
|
-
}
|
|
27
|
-
// Use path.basename to get the repo name from the full path
|
|
28
|
-
const repoName = path.basename(repoRoot); // Get the last part of the path (the repo name)
|
|
29
|
-
const scaiRepoPath = path.join(SCAI_HOME, 'repos', repoName, 'db.sqlite');
|
|
30
|
-
return scaiRepoPath;
|
|
31
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { handleAgentRun } from './agentManager.js';
|
|
|
30
30
|
import { addCommentsModule } from './pipeline/modules/commentModule.js';
|
|
31
31
|
import { generateTestsModule } from './pipeline/modules/generateTestsModule.js';
|
|
32
32
|
import { preserveCodeModule } from './pipeline/modules/preserveCodeModule.js';
|
|
33
|
+
import { runInteractiveDelete } from './commands/DeleteIndex.js';
|
|
33
34
|
// 🎛️ CLI Setup
|
|
34
35
|
const cmd = new Command('scai')
|
|
35
36
|
.version(version)
|
|
@@ -177,6 +178,12 @@ index
|
|
|
177
178
|
.action(() => {
|
|
178
179
|
runInteractiveSwitch();
|
|
179
180
|
});
|
|
181
|
+
index
|
|
182
|
+
.command('delete')
|
|
183
|
+
.description('Delete a repository from the index (interactive)')
|
|
184
|
+
.action(() => {
|
|
185
|
+
runInteractiveDelete();
|
|
186
|
+
});
|
|
180
187
|
// This will help resolve the current directory in an ES Module
|
|
181
188
|
cmd
|
|
182
189
|
.command('check-db')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { normalizePath } from './normalizePath.js';
|
|
4
|
+
/**
|
|
5
|
+
* Generate a stable unique key for a repo path.
|
|
6
|
+
* Uses the basename plus a short hash of the full path.
|
|
7
|
+
* Example: "sps-1a2b3c"
|
|
8
|
+
*/
|
|
9
|
+
export function getHashedRepoKey(repoPath) {
|
|
10
|
+
const absPath = normalizePath(repoPath); // now cross-platform consistent
|
|
11
|
+
const base = path.basename(absPath);
|
|
12
|
+
const hash = crypto.createHash('md5').update(absPath).digest('hex').slice(0, 6);
|
|
13
|
+
return `${base}-${hash}`;
|
|
14
|
+
}
|