scai 0.1.107 β†’ 0.1.109

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
@@ -559,7 +559,7 @@ You can run it in two ways:
559
559
 
560
560
  ---
561
561
 
562
- ### Backup only of `~/.scai`:**
562
+ ### Backup only of `~/.scai`:
563
563
 
564
564
  Creates a backup of the .scai folder in the user root dir.
565
565
 
@@ -569,7 +569,8 @@ Creates a backup of the .scai folder in the user root dir.
569
569
 
570
570
  Results in this:
571
571
  ~/.scai_backup_2025-08-12T06-58-00-227Z/
572
- ---
572
+
573
+ </br>
573
574
 
574
575
  ## πŸ” License & Fair Use
575
576
 
package/dist/CHANGELOG.md CHANGED
@@ -162,4 +162,13 @@ Type handling with the module pipeline
162
162
  ## 2025-08-31
163
163
 
164
164
  β€’ Update Spinner class to correctly display text and frames
165
- β€’ Update CLI help to reflect new agent workflow examples.
165
+ β€’ Update CLI help to reflect new agent workflow examples.
166
+
167
+ ## 2025-09-01
168
+
169
+ * Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
170
+
171
+ ## 2025-09-02
172
+
173
+ β€’ Added test configuration for project and generated tests
174
+ β€’ Add runTestsModule and repairTestsModule for testing pipeline
@@ -0,0 +1,10 @@
1
+ import { SCAI_HOME } from "../constants"; // example constant
2
+ // cli/src/__tests__/example.test.ts
3
+ describe("CLI src basic test", () => {
4
+ it("should pass a simple truthy test", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ it("should import a constant from cli/src/constants.ts", () => {
8
+ expect(typeof SCAI_HOME).not.toBe("undefined");
9
+ });
10
+ });
@@ -18,8 +18,20 @@ export class Agent {
18
18
  // Resolve modules (with before/after dependencies)
19
19
  const modules = this.resolveModules(this.goals);
20
20
  console.log(chalk.green("πŸ“‹ Modules to run:"), modules.map((m) => m.name).join(" β†’ "));
21
- // Read file content (optional, could be used by modules in workflow)
22
- await fs.readFile(filepath, "utf-8");
21
+ try {
22
+ // Check that the file exists before trying to read it
23
+ await fs.access(filepath);
24
+ // Read file content (optional, could be used by modules in workflow)
25
+ await fs.readFile(filepath, "utf-8");
26
+ }
27
+ catch (err) {
28
+ if (err.code === "ENOENT") {
29
+ console.error(chalk.redBright("❌ Error:"), `File not found: ${chalk.yellow(filepath)}`);
30
+ console.error(`Make sure the path is correct. (cwd: ${chalk.gray(process.cwd())})`);
31
+ process.exit(1);
32
+ }
33
+ throw err; // rethrow for unexpected errors
34
+ }
23
35
  // Delegate everything to handleAgentRun (like CLI commands do)
24
36
  await handleAgentRun(filepath, modules);
25
37
  console.log(chalk.green("βœ… Agent finished!"));
@@ -76,6 +76,11 @@ export async function handleAgentRun(filepath, modules) {
76
76
  baseChunks.length = 0;
77
77
  baseChunks.push(...reset);
78
78
  break;
79
+ case 'skip':
80
+ console.log(chalk.gray(`⏭️ Skipped writing for module ${mod.name}`));
81
+ // don’t touch files, but keep chunks flowing
82
+ workingChunks = processed;
83
+ break;
79
84
  default:
80
85
  console.log(chalk.yellow(`⚠️ Unknown mode; skipping write`));
81
86
  // still move pipeline forward with processed
@@ -15,12 +15,19 @@ function runGitCommand(cmd, cwd) {
15
15
  */
16
16
  function getRepoOwnerAndNameFromGit(indexDir) {
17
17
  try {
18
- const originUrl = runGitCommand('git config --get remote.origin.url', indexDir);
18
+ const originUrl = runGitCommand('git config --get remote.origin.url', indexDir).trim();
19
19
  console.log(`πŸ”— Git origin URL from '${indexDir}': ${originUrl}`);
20
- const match = originUrl.match(/github[^:/]*[:/](.+?)(?:\.git)?$/);
20
+ // Handle both SSH and HTTPS formats, with or without extra slashes
21
+ // Examples:
22
+ // - git@github.com:owner/repo.git
23
+ // - git@github.com:/owner/repo.git
24
+ // - https://github.com/owner/repo.git
25
+ const match = originUrl.match(/github[^:/]*[:/]\/?(.+?)(?:\.git)?$/);
21
26
  if (!match)
22
27
  throw new Error("❌ Could not parse GitHub repo from origin URL.");
23
28
  const [owner, repo] = match[1].split('/');
29
+ if (!owner || !repo)
30
+ throw new Error("❌ Failed to extract owner or repo name from URL.");
24
31
  console.log(`βœ… Parsed from Git: owner='${owner}', repo='${repo}'`);
25
32
  return { owner, repo };
26
33
  }
@@ -7,16 +7,27 @@ export const cleanGeneratedTestsModule = {
7
7
  // normalize + strip markdown
8
8
  const normalized = normalizeText(content);
9
9
  const stripped = stripMarkdownFences(normalized);
10
- // filter non-code lines
11
- const lines = stripped.split("\n");
12
10
  // filter non-code lines, but keep blank ones
11
+ const lines = stripped.split("\n");
13
12
  const codeLines = lines.filter(line => line.trim() === "" || isCodeLike(line));
14
- const cleanedCode = codeLines.join("\n");
13
+ // remove duplicate imports (normalize spacing/semicolon)
14
+ const seenImports = new Set();
15
+ const dedupedLines = codeLines.filter(line => {
16
+ if (line.trim().startsWith("import")) {
17
+ const key = line.trim().replace(/;$/, "");
18
+ if (seenImports.has(key)) {
19
+ return false;
20
+ }
21
+ seenImports.add(key);
22
+ }
23
+ return true;
24
+ });
25
+ const cleanedCode = dedupedLines.join("\n").trimEnd();
15
26
  return {
16
27
  originalContent: content,
17
- content: cleanedCode, // cleaned code for pipeline
18
- filepath, // original file path
19
- mode: "overwrite", // indicates overwrite existing file
28
+ content: cleanedCode,
29
+ filepath,
30
+ mode: "overwrite",
20
31
  };
21
32
  }
22
33
  };
@@ -0,0 +1,40 @@
1
+ import { generate } from "../../lib/generate.js";
2
+ import { Config } from "../../config.js";
3
+ export const repairTestsModule = {
4
+ name: "repairTestsModule",
5
+ description: "Fix failing Jest tests using AI",
6
+ async run({ content, filepath, summary }) {
7
+ const model = Config.getModel();
8
+ const prompt = `
9
+ You are a senior engineer tasked with repairing Jest tests.
10
+
11
+ The following test file failed:
12
+
13
+ --- BEGIN TEST FILE ---
14
+ ${content}
15
+ --- END TEST FILE ---
16
+
17
+ Failure summary:
18
+ ${summary || "No summary provided."}
19
+
20
+ Instructions:
21
+ - Keep the overall structure, imports, and test cases.
22
+ - Only fix syntax errors, invalid Jest matchers, or broken references.
23
+ - Do NOT remove or replace entire test suites unless strictly necessary.
24
+ - Do NOT generate trivial placeholder tests (like add(2,3) examples).
25
+ - Only return valid Jest code, no explanations, no markdown fences.
26
+
27
+ Output the repaired test file:
28
+ `.trim();
29
+ const response = await generate({ content: prompt }, model);
30
+ if (!response)
31
+ throw new Error("⚠️ No repaired test code returned from model");
32
+ return {
33
+ originalContent: content,
34
+ content: response.content, // repaired test code
35
+ filepath, // repair in-place
36
+ summary: `Repaired tests based on failure: ${summary || "unknown error"}`,
37
+ mode: "overwrite" // signal to overwrite existing test file
38
+ };
39
+ }
40
+ };
@@ -0,0 +1,37 @@
1
+ import { execSync } from "child_process";
2
+ import path from "path";
3
+ export const runTestsModule = {
4
+ name: "runTestsModule",
5
+ description: "Runs generated Jest tests with safety checks for a single file",
6
+ async run(input) {
7
+ const { filepath } = input;
8
+ if (!filepath) {
9
+ throw new Error("No filepath provided to runTestsModule.");
10
+ }
11
+ const absoluteFilePath = path.resolve(filepath);
12
+ try {
13
+ // Step 1: TypeScript syntax check for this file only using tsconfig.test.json
14
+ // Step 1: TypeScript syntax check for this file only
15
+ execSync(`npx tsc --noEmit --project tsconfig.test.json`, { stdio: "inherit" });
16
+ // Step 2: Dry-run Jest for this file only
17
+ execSync(`npx jest --config ${path.resolve("jest.config.ts")} --dryRun ${absoluteFilePath}`, { stdio: "inherit" });
18
+ // Step 3: Full Jest run for this file only
19
+ execSync(`npx jest --config ${path.resolve("jest.config.ts")} ${absoluteFilePath}`, { stdio: "inherit" });
20
+ return {
21
+ content: "βœ… Tests ran successfully",
22
+ filepath,
23
+ mode: "skip",
24
+ summary: "All tests passed successfully."
25
+ };
26
+ }
27
+ catch (error) {
28
+ const errorMessage = error?.message || String(error);
29
+ return {
30
+ content: `❌ Test run failed:\n${errorMessage}`,
31
+ filepath,
32
+ mode: "skip",
33
+ summary: errorMessage // provides failure context for repair
34
+ };
35
+ }
36
+ }
37
+ };
@@ -6,6 +6,8 @@ import { commitSuggesterModule } from '../modules/commitSuggesterModule.js';
6
6
  import { changelogModule } from '../modules/changeLogModule.js';
7
7
  import { cleanGeneratedTestsModule } from '../modules/cleanGeneratedTestsModule.js';
8
8
  import { preserveCodeModule } from '../modules/preserveCodeModule.js';
9
+ import { runTestsModule } from '../modules/runTestsModule.js';
10
+ import { repairTestsModule } from '../modules/repairTestsModule.js';
9
11
  // Built-in modules with metadata
10
12
  export const builtInModules = {
11
13
  comments: {
@@ -38,6 +40,21 @@ export const builtInModules = {
38
40
  ...cleanGeneratedTestsModule,
39
41
  group: 'testing',
40
42
  },
43
+ runTests: {
44
+ ...runTestsModule,
45
+ group: 'testing',
46
+ dependencies: {
47
+ before: ['tests'], // must exist after tests are generated
48
+ after: ['cleanTests'], // run after cleaning
49
+ },
50
+ },
51
+ repairTests: {
52
+ ...repairTestsModule,
53
+ group: 'testing',
54
+ dependencies: {
55
+ after: ['runTests'], // repair runs after tests have been executed
56
+ },
57
+ },
41
58
  suggest: {
42
59
  ...commitSuggesterModule,
43
60
  group: 'git',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -1,11 +0,0 @@
1
- const config = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- setupFilesAfterEnv: ['./jest.setup.ts'],
5
- transform: {
6
- '^.+\\.ts$': 'ts-jest',
7
- },
8
- moduleFileExtensions: ['ts', 'js', 'json', 'node'],
9
- testPathIgnorePatterns: ['/node_modules/', '/dist/'],
10
- };
11
- export default config;
@@ -1,14 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- // jest.setup.ts
3
- // Mock the global fetch function in Jest for Node.js 18+
4
- global.fetch = jest.fn(() => Promise.resolve({
5
- json: () => Promise.resolve({ response: 'Mocked Commit Message' }),
6
- ok: true,
7
- status: 200,
8
- statusText: 'OK',
9
- headers: new Headers(),
10
- redirected: false,
11
- type: 'default',
12
- url: 'https://mocked-url.com',
13
- }) // Type assertion for `Response` object
14
- );
@@ -1,11 +0,0 @@
1
- import { execSync } from 'child_process';
2
- export function runJestTest(testFile) {
3
- try {
4
- execSync(`npx jest ${testFile} --silent`, { stdio: 'inherit' });
5
- return true;
6
- }
7
- catch (err) {
8
- console.error(`❌ Tests failed for ${testFile}`);
9
- return false;
10
- }
11
- }