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 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 { normalizePath, getRepoKeyForPath } from '../utils/normalizePath.js';
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
- const normalizedInput = normalizePath(inputPathOrKey);
9
- // Try to match by key directly
10
- if (config.repos[normalizedInput]) {
11
- config.activeRepo = normalizedInput;
12
- // Update GitHub token
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
- // Try to match by indexDir path
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
- // Ensure the active repo change is saved back to the config
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 { getRepoKeyForPath, normalizePath } from './utils/normalizePath.js';
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 config = readConfig();
82
- const activeRepo = config.activeRepo;
83
- if (activeRepo) {
84
- const normalized = normalizePath(activeRepo);
85
- return normalizePath(config.repos[normalized]?.indexDir ?? '');
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
- const absPath = path.resolve(indexDir); // Resolve the index directory to an absolute path
91
- const repoKey = normalizePath(absPath); // Normalize path for the repo (get repo name, not full path)
92
- // Ensure repoKey doesn't contain an absolute path, only the repo name or a relative path
93
- const scaiRepoRoot = path.join(SCAI_REPOS, path.basename(repoKey)); // Use repo name as key to avoid double paths
94
- // Set the active repo to the provided indexDir
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
- // Init DB if not exists
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(); // Now DB creation works after config update
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[normalizedRepoPath]) {
117
- cfg.repos[normalizedRepoPath] = {};
118
- }
119
- cfg.repos[normalizedRepoPath] = {
120
- ...cfg.repos[normalizedRepoPath],
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); // Persist the config update
124
- console.log(`✅ Repo index directory set for ${normalizedRepoPath} : ${normalizedIndexDir}`);
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 = normalizePath(repoKey);
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)`) // Active repo in green
146
- : chalk.white(` ${key}`); // Inactive repos in white
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
- // Normalize the active repo path and fetch token from repos[activeRepo]
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
- const repoKey = getRepoKeyForPath(active, cfg) ?? normalizePath(active);
168
- if (!cfg.repos[repoKey]) {
169
- cfg.repos[repoKey] = {};
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?.[active] : {};
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 { Config } from '../config.js';
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 getDbForRepo() {
11
- const repoRoot = Config.getIndexDir();
12
- if (!repoRoot) {
13
- throw new Error('No index directory set. Please set an index directory first.');
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
- fs.mkdirSync(SCAI_HOME, { recursive: true });
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"