octboos 0.1.2
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 +53 -0
- package/bin/octboos.js +38 -0
- package/package.json +48 -0
- package/src/adapters/index.js +116 -0
- package/src/commands/init.js +259 -0
- package/src/commands/status.js +77 -0
- package/src/commands/sync.js +55 -0
- package/src/index.js +2 -0
- package/src/indexer/scanner.js +285 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xoner
|
|
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
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
21
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# octboos
|
|
2
|
+
|
|
3
|
+
> One agent to control every AI tool.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/octboos)
|
|
6
|
+
[](https://www.npmjs.com/package/octboos)
|
|
7
|
+
[](https://www.npmjs.com/package/octboos)
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
AI coding tools start from zero every session. They don't remember your project structure, dependencies, or conventions, wasting tokens and breaking context.
|
|
11
|
+
|
|
12
|
+
## The Solution
|
|
13
|
+
octboos creates a `.agent/` directory in your project—a persistent memory bank that any AI tool can read to understand your codebase instantly.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
```bash
|
|
17
|
+
npx octboos init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it generates
|
|
21
|
+
The `.agent/` directory contains:
|
|
22
|
+
```
|
|
23
|
+
.agent/
|
|
24
|
+
├── config.json # Octboos configuration and selected AI tools
|
|
25
|
+
├── map.md # Project structure, dependencies, and tech stack
|
|
26
|
+
├── style.md # Your coding conventions and preferences
|
|
27
|
+
└── wiki/ # Auto-generated documentation for modules and components
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Supported AI Tools
|
|
31
|
+
| Tool | Configuration File |
|
|
32
|
+
|---------------|--------------------|
|
|
33
|
+
| Claude Code | CLAUDE.md |
|
|
34
|
+
| OpenCode | AGENTS.md |
|
|
35
|
+
| Cursor | .cursor/rules |
|
|
36
|
+
| Aider | .aider.conf.yml |
|
|
37
|
+
| Gemini CLI | GEMINI.md |
|
|
38
|
+
| Codex CLI | (no file, uses context) |
|
|
39
|
+
| Windsurf | .windsurfrules |
|
|
40
|
+
| GitHub Copilot| .github/copilot-instructions.md |
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
- `octboos init` — Initialize octboos in your project (creates .agent/)
|
|
44
|
+
- `octboos sync` — Update .agent/ with latest project changes
|
|
45
|
+
- `octboos status` — Show current octboos configuration and file stats
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
1. **Scan** — octboos analyzes your project structure, dependencies, and files
|
|
49
|
+
2. **Generate** — creates/updates .agent/ with context for AI tools
|
|
50
|
+
3. **AI reads** — your AI tool loads .agent/ files to understand your project instantly
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
MIT
|
package/bin/octboos.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('octboos')
|
|
12
|
+
.description('One agent to control every AI tool')
|
|
13
|
+
.version(pkg.version);
|
|
14
|
+
|
|
15
|
+
// Commands
|
|
16
|
+
const { initCommand } = await import('../src/commands/init.js');
|
|
17
|
+
const { syncCommand } = await import('../src/commands/sync.js');
|
|
18
|
+
const { statusCommand } = await import('../src/commands/status.js');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('init')
|
|
22
|
+
.description('Initialize Octboos in current project')
|
|
23
|
+
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
24
|
+
.option('--adapters <list>', 'Comma-separated adapters: claude,codex,cursor,windsurf,copilot')
|
|
25
|
+
.action(initCommand);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('sync')
|
|
29
|
+
.description('Sync and update .agent/ wiki and context')
|
|
30
|
+
.option('--incremental', 'Only update changed files')
|
|
31
|
+
.action(syncCommand);
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('status')
|
|
35
|
+
.description('Show current Octboos status for this project')
|
|
36
|
+
.action(statusCommand);
|
|
37
|
+
|
|
38
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "octboos",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "One agent to control every AI tool",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"claude",
|
|
9
|
+
"codex",
|
|
10
|
+
"cursor",
|
|
11
|
+
"context",
|
|
12
|
+
"memory",
|
|
13
|
+
"wiki"
|
|
14
|
+
],
|
|
15
|
+
"author": "xoner",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"octboos": "bin/octboos.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./src/index.js",
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "node bin/octboos.js",
|
|
28
|
+
"test": "node --test"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Xoner1/octboos",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Xoner1/octboos.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Xoner1/octboos/issues"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@inquirer/prompts": "^3.3.0",
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"fast-glob": "^3.3.2",
|
|
43
|
+
"ora": "^8.0.1"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// All supported AI tool adapters
|
|
2
|
+
export const ADAPTERS = {
|
|
3
|
+
claude: {
|
|
4
|
+
name: 'Claude Code',
|
|
5
|
+
file: 'CLAUDE.md',
|
|
6
|
+
description: 'Anthropic Claude Code',
|
|
7
|
+
},
|
|
8
|
+
codex: {
|
|
9
|
+
name: 'Codex / OpenCode',
|
|
10
|
+
file: 'AGENTS.md',
|
|
11
|
+
description: 'OpenAI Codex CLI & OpenCode',
|
|
12
|
+
},
|
|
13
|
+
cursor: {
|
|
14
|
+
name: 'Cursor',
|
|
15
|
+
file: '.cursor/rules',
|
|
16
|
+
description: 'Cursor IDE',
|
|
17
|
+
},
|
|
18
|
+
windsurf: {
|
|
19
|
+
name: 'Windsurf',
|
|
20
|
+
file: '.windsurfrules',
|
|
21
|
+
description: 'Windsurf IDE',
|
|
22
|
+
},
|
|
23
|
+
copilot: {
|
|
24
|
+
name: 'GitHub Copilot',
|
|
25
|
+
file: '.github/copilot-instructions.md',
|
|
26
|
+
description: 'GitHub Copilot',
|
|
27
|
+
},
|
|
28
|
+
gemini: {
|
|
29
|
+
name: 'Gemini CLI',
|
|
30
|
+
file: 'GEMINI.md',
|
|
31
|
+
description: 'Google Gemini CLI',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function generateAdapterContent(adapterKey, projectName) {
|
|
36
|
+
const header = buildHeader(projectName);
|
|
37
|
+
|
|
38
|
+
switch (adapterKey) {
|
|
39
|
+
case 'claude':
|
|
40
|
+
return `${header}
|
|
41
|
+
|
|
42
|
+
## Octboos Instructions
|
|
43
|
+
> This file is auto-managed by Octboos. Edit \`.agent/\` files instead.
|
|
44
|
+
|
|
45
|
+
## Before Every Task
|
|
46
|
+
1. Read \`.agent/map.md\` for project structure
|
|
47
|
+
2. Check \`.agent/wiki/\` for module documentation
|
|
48
|
+
3. Follow conventions in \`.agent/style.md\`
|
|
49
|
+
4. After completing a task, update relevant \`.agent/wiki/\` files
|
|
50
|
+
|
|
51
|
+
## Quick Reference
|
|
52
|
+
- Project map: \`.agent/map.md\`
|
|
53
|
+
- Code wiki: \`.agent/wiki/\`
|
|
54
|
+
- User style: \`.agent/style.md\`
|
|
55
|
+
- Octboos config: \`.agent/config.json\`
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
case 'codex':
|
|
59
|
+
return `${header}
|
|
60
|
+
|
|
61
|
+
## Octboos Context
|
|
62
|
+
> Auto-managed by Octboos. Source of truth: \`.agent/\`
|
|
63
|
+
|
|
64
|
+
## Required Reading
|
|
65
|
+
Before any task, read:
|
|
66
|
+
- \`.agent/map.md\` — project structure and stack
|
|
67
|
+
- \`.agent/wiki/\` — module documentation
|
|
68
|
+
- \`.agent/style.md\` — coding conventions
|
|
69
|
+
|
|
70
|
+
## Update Rule
|
|
71
|
+
After completing any task, sync relevant docs:
|
|
72
|
+
\`\`\`
|
|
73
|
+
npx octboos sync --incremental
|
|
74
|
+
\`\`\`
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
case 'cursor':
|
|
78
|
+
case 'windsurf':
|
|
79
|
+
return `# Octboos Context for ${ADAPTERS[adapterKey].name}
|
|
80
|
+
> Auto-managed. Edit .agent/ files instead.
|
|
81
|
+
|
|
82
|
+
Always read .agent/map.md before starting any task.
|
|
83
|
+
Follow conventions in .agent/style.md.
|
|
84
|
+
Reference .agent/wiki/ for module documentation.
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
case 'copilot':
|
|
88
|
+
return `# GitHub Copilot Instructions
|
|
89
|
+
> Managed by Octboos
|
|
90
|
+
|
|
91
|
+
## Context Files
|
|
92
|
+
- \`.agent/map.md\` — project map and stack
|
|
93
|
+
- \`.agent/wiki/\` — module documentation
|
|
94
|
+
- \`.agent/style.md\` — code style and conventions
|
|
95
|
+
|
|
96
|
+
Always reference these files before suggesting code changes.
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
case 'gemini':
|
|
100
|
+
return `${header}
|
|
101
|
+
|
|
102
|
+
## Octboos Context
|
|
103
|
+
Read \`.agent/map.md\` and \`.agent/wiki/\` before any task.
|
|
104
|
+
Follow \`.agent/style.md\` for code conventions.
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
return header;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildHeader(projectName) {
|
|
113
|
+
return `# ${projectName || 'Project'} — Octboos Context
|
|
114
|
+
> Last synced: ${new Date().toISOString().split('T')[0]}
|
|
115
|
+
> Managed by Octboos (https://github.com/Octboos1/octboos.git)`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { input, checkbox, confirm } from '@inquirer/prompts';
|
|
6
|
+
import { scanProject, buildFileMap, detectInstalledTools } from '../indexer/scanner.js';
|
|
7
|
+
import { ADAPTERS, generateAdapterContent } from '../adapters/index.js';
|
|
8
|
+
|
|
9
|
+
const AGENT_DIR = '.agent';
|
|
10
|
+
|
|
11
|
+
export async function initCommand(options) {
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(chalk.bold.cyan(' ╔═══════════════════════════════╗'));
|
|
14
|
+
console.log(chalk.bold.cyan(' ║ Octboos — Init ║'));
|
|
15
|
+
console.log(chalk.bold.cyan(' ║ Transform your AI tool ║'));
|
|
16
|
+
console.log(chalk.bold.cyan(' ╚═══════════════════════════════╝'));
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
|
|
21
|
+
// Check if already initialized
|
|
22
|
+
if (existsSync(join(cwd, AGENT_DIR, 'config.json'))) {
|
|
23
|
+
console.log(chalk.yellow(' ⚠ Octboos already initialized in this project.'));
|
|
24
|
+
console.log(chalk.gray(' Run ' + chalk.white('npx octboos sync') + ' to update.'));
|
|
25
|
+
console.log('');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Step 0 — Detect installed AI tools
|
|
30
|
+
let detectedTools = { detected: [], uncertain: [], notFound: [] };
|
|
31
|
+
try {
|
|
32
|
+
detectedTools = detectInstalledTools();
|
|
33
|
+
if (detectedTools.detected.length > 0) {
|
|
34
|
+
console.log(chalk.gray(` 🔍 Found ${detectedTools.detected.length} tools on your machine`));
|
|
35
|
+
console.log('');
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Silently fall back to manual selection if detection fails
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 1 — Project name
|
|
42
|
+
let projectName;
|
|
43
|
+
try {
|
|
44
|
+
const pkgPath = join(cwd, 'package.json');
|
|
45
|
+
projectName = existsSync(pkgPath)
|
|
46
|
+
? JSON.parse(readFileSync(pkgPath, 'utf8')).name || ''
|
|
47
|
+
: '';
|
|
48
|
+
} catch { projectName = ''; }
|
|
49
|
+
|
|
50
|
+
if (!options.yes) {
|
|
51
|
+
projectName = await input({
|
|
52
|
+
message: 'Project name:',
|
|
53
|
+
default: projectName || 'my-project',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 2 — Choose adapters
|
|
58
|
+
let selectedAdapters = [];
|
|
59
|
+
|
|
60
|
+
if (!options.yes && !options.adapters) {
|
|
61
|
+
// Build choices with pre-checked detected tools
|
|
62
|
+
const choices = Object.entries(ADAPTERS).map(([key, val]) => {
|
|
63
|
+
let checked = false;
|
|
64
|
+
let name = `${val.name} (${val.file})`;
|
|
65
|
+
|
|
66
|
+
// Check if detected
|
|
67
|
+
if (detectedTools.detected.includes(key)) {
|
|
68
|
+
checked = true;
|
|
69
|
+
}
|
|
70
|
+
// Check if uncertain
|
|
71
|
+
else if (detectedTools.uncertain.includes(key)) {
|
|
72
|
+
name += ' (not sure)';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
value: key,
|
|
78
|
+
checked
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
selectedAdapters = await checkbox({
|
|
83
|
+
message: 'Which AI tools do you use?',
|
|
84
|
+
choices
|
|
85
|
+
});
|
|
86
|
+
} else if (options.adapters) {
|
|
87
|
+
selectedAdapters = options.adapters.split(',').map(s => s.trim());
|
|
88
|
+
} else {
|
|
89
|
+
// Default fallback when options.yes is true
|
|
90
|
+
selectedAdapters = ['claude', 'codex'];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Step 3 — Scan project
|
|
94
|
+
const spinner = ora({
|
|
95
|
+
text: chalk.gray('Scanning project...'),
|
|
96
|
+
color: 'cyan',
|
|
97
|
+
}).start();
|
|
98
|
+
|
|
99
|
+
let scanResult;
|
|
100
|
+
try {
|
|
101
|
+
scanResult = await scanProject(cwd);
|
|
102
|
+
spinner.succeed(chalk.green(`Scanned ${scanResult.fileCount} files`));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
spinner.fail('Failed to scan project');
|
|
105
|
+
console.error(err);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Step 4 — Create .agent/ directory
|
|
110
|
+
const agentDir = join(cwd, AGENT_DIR);
|
|
111
|
+
const wikiDir = join(agentDir, 'wiki');
|
|
112
|
+
|
|
113
|
+
mkdirSync(agentDir, { recursive: true });
|
|
114
|
+
mkdirSync(wikiDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
// Step 5 — Generate files
|
|
117
|
+
const genSpinner = ora({ text: chalk.gray('Generating context files...'), color: 'cyan' }).start();
|
|
118
|
+
|
|
119
|
+
// config.json
|
|
120
|
+
const config = {
|
|
121
|
+
version: '0.1.0',
|
|
122
|
+
projectName,
|
|
123
|
+
adapters: selectedAdapters,
|
|
124
|
+
autoSync: true,
|
|
125
|
+
created: new Date().toISOString(),
|
|
126
|
+
lastSync: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
writeFileSync(join(agentDir, 'config.json'), JSON.stringify(config, null, 2));
|
|
129
|
+
|
|
130
|
+
// map.md
|
|
131
|
+
const mapContent = buildFileMap(scanResult, cwd);
|
|
132
|
+
writeFileSync(join(agentDir, 'map.md'), mapContent);
|
|
133
|
+
|
|
134
|
+
// style.md
|
|
135
|
+
writeFileSync(join(agentDir, 'style.md'), buildStyleTemplate(projectName));
|
|
136
|
+
|
|
137
|
+
// wiki/INDEX.md
|
|
138
|
+
writeFileSync(join(wikiDir, 'INDEX.md'), buildWikiIndex(projectName, scanResult));
|
|
139
|
+
|
|
140
|
+
genSpinner.succeed(chalk.green('Generated .agent/ context files'));
|
|
141
|
+
|
|
142
|
+
// Step 6 — Generate adapter files
|
|
143
|
+
const adapterSpinner = ora({ text: chalk.gray('Configuring AI tools...'), color: 'cyan' }).start();
|
|
144
|
+
|
|
145
|
+
const generatedAdapters = [];
|
|
146
|
+
for (const adapterKey of selectedAdapters) {
|
|
147
|
+
const adapter = ADAPTERS[adapterKey];
|
|
148
|
+
if (!adapter) continue;
|
|
149
|
+
|
|
150
|
+
const filePath = join(cwd, adapter.file);
|
|
151
|
+
const fileDir = dirname(filePath);
|
|
152
|
+
|
|
153
|
+
mkdirSync(fileDir, { recursive: true });
|
|
154
|
+
|
|
155
|
+
// Don't overwrite existing files — append Octboos section
|
|
156
|
+
if (existsSync(filePath)) {
|
|
157
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
158
|
+
if (!existing.includes('Octboos')) {
|
|
159
|
+
writeFileSync(filePath, existing + '\n\n' + generateAdapterContent(adapterKey, projectName));
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
writeFileSync(filePath, generateAdapterContent(adapterKey, projectName));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
generatedAdapters.push(adapter.file);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
adapterSpinner.succeed(chalk.green(`Configured ${generatedAdapters.length} AI tools`));
|
|
169
|
+
|
|
170
|
+
// Step 7 — Add to .gitignore if needed (keep .agent/ tracked)
|
|
171
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
172
|
+
if (existsSync(gitignorePath)) {
|
|
173
|
+
const gitignore = readFileSync(gitignorePath, 'utf8');
|
|
174
|
+
if (!gitignore.includes('.agent/graph.html')) {
|
|
175
|
+
writeFileSync(gitignorePath, gitignore + '\n# Octboos - keep .agent/ tracked except large files\n.agent/graph.html\n');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Done!
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(chalk.bold.green(' ✓ Octboos initialized successfully!'));
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.gray(' Created:'));
|
|
184
|
+
console.log(chalk.white(' .agent/'));
|
|
185
|
+
console.log(chalk.gray(' ├── ') + chalk.white('config.json ') + chalk.gray('← Octboos config'));
|
|
186
|
+
console.log(chalk.gray(' ├── ') + chalk.white('map.md ') + chalk.gray('← project structure'));
|
|
187
|
+
console.log(chalk.gray(' ├── ') + chalk.white('style.md ') + chalk.gray('← coding conventions'));
|
|
188
|
+
console.log(chalk.gray(' └── ') + chalk.white('wiki/ ') + chalk.gray('← auto documentation'));
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(chalk.gray(' AI configs:'));
|
|
191
|
+
for (const f of generatedAdapters) {
|
|
192
|
+
console.log(chalk.gray(' ✓ ') + chalk.white(f));
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(chalk.bold(' Next steps:'));
|
|
196
|
+
console.log(chalk.cyan(' 1.') + chalk.white(' Edit .agent/style.md with your coding conventions'));
|
|
197
|
+
console.log(chalk.cyan(' 2.') + chalk.white(' Run npx octboos sync to update wiki'));
|
|
198
|
+
console.log(chalk.cyan(' 3.') + chalk.white(' Commit .agent/ to git'));
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildStyleTemplate(projectName) {
|
|
203
|
+
return `# Coding Style — ${projectName}
|
|
204
|
+
> Edit this file to teach Octboos your preferences.
|
|
205
|
+
> Last updated: ${new Date().toISOString().split('T')[0]}
|
|
206
|
+
|
|
207
|
+
## General Conventions
|
|
208
|
+
- [ ] Add your naming conventions here
|
|
209
|
+
- [ ] Add your file structure preferences here
|
|
210
|
+
|
|
211
|
+
## Code Style
|
|
212
|
+
- [ ] Indentation: (tabs / 2 spaces / 4 spaces)
|
|
213
|
+
- [ ] Quotes: (single / double)
|
|
214
|
+
- [ ] Semicolons: (yes / no)
|
|
215
|
+
|
|
216
|
+
## Architecture Patterns
|
|
217
|
+
- [ ] Describe your patterns here (MVC, Repository, etc.)
|
|
218
|
+
|
|
219
|
+
## What to Avoid
|
|
220
|
+
- [ ] List anti-patterns specific to this project
|
|
221
|
+
|
|
222
|
+
## Useful Commands
|
|
223
|
+
\`\`\`bash
|
|
224
|
+
# Add your most-used project commands here
|
|
225
|
+
# npm run dev
|
|
226
|
+
# npm run build
|
|
227
|
+
# npm test
|
|
228
|
+
\`\`\`
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildWikiIndex(projectName, scanResult) {
|
|
233
|
+
const lines = [
|
|
234
|
+
`# Wiki — ${projectName}`,
|
|
235
|
+
`> Auto-generated by Octboos | ${new Date().toISOString().split('T')[0]}`,
|
|
236
|
+
'',
|
|
237
|
+
'## Modules',
|
|
238
|
+
'> Add module documentation here or run `npx octboos sync` to auto-generate.',
|
|
239
|
+
'',
|
|
240
|
+
'## Stack Summary',
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
if (scanResult.frameworks.length > 0) {
|
|
244
|
+
lines.push(`**Frameworks:** ${scanResult.frameworks.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
if (scanResult.stack.length > 0) {
|
|
247
|
+
lines.push(`**Libraries:** ${scanResult.stack.join(', ')}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const langs = Object.entries(scanResult.languages)
|
|
251
|
+
.sort((a, b) => b[1] - a[1])
|
|
252
|
+
.slice(0, 3)
|
|
253
|
+
.map(([l]) => l);
|
|
254
|
+
if (langs.length > 0) {
|
|
255
|
+
lines.push(`**Languages:** ${langs.join(', ')}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function statusCommand() {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
const configPath = join(cwd, '.agent', 'config.json');
|
|
8
|
+
|
|
9
|
+
console.log('');
|
|
10
|
+
|
|
11
|
+
if (!existsSync(configPath)) {
|
|
12
|
+
console.log(chalk.yellow(' ✗ Octboos not initialized in this project'));
|
|
13
|
+
console.log(chalk.gray(' Run: ') + chalk.white('npx octboos init'));
|
|
14
|
+
console.log('');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
19
|
+
const lastSync = new Date(config.lastSync);
|
|
20
|
+
const hoursSince = Math.floor((Date.now() - lastSync) / 1000 / 60 / 60);
|
|
21
|
+
|
|
22
|
+
console.log(chalk.bold.cyan(' Octboos Status'));
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(chalk.gray(' Project: ') + chalk.white(config.projectName));
|
|
25
|
+
console.log(chalk.gray(' Version: ') + chalk.white(config.version));
|
|
26
|
+
console.log(chalk.gray(' Last sync: ') + (
|
|
27
|
+
hoursSince < 24
|
|
28
|
+
? chalk.green(`${hoursSince}h ago`)
|
|
29
|
+
: chalk.yellow(`${Math.floor(hoursSince / 24)}d ago — run octboos sync`)
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// Files
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.gray(' Context files:'));
|
|
35
|
+
const agentFiles = [
|
|
36
|
+
['map.md', 'Project structure'],
|
|
37
|
+
['style.md', 'Coding style'],
|
|
38
|
+
['config.json', 'Octboos config'],
|
|
39
|
+
];
|
|
40
|
+
for (const [file, desc] of agentFiles) {
|
|
41
|
+
const exists = existsSync(join(cwd, '.agent', file));
|
|
42
|
+
console.log(
|
|
43
|
+
' ' + (exists ? chalk.green('✓') : chalk.red('✗')) + ' ' +
|
|
44
|
+
chalk.white(`.agent/${file}`) + chalk.gray(` — ${desc}`)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wiki
|
|
49
|
+
const wikiDir = join(cwd, '.agent', 'wiki');
|
|
50
|
+
if (existsSync(wikiDir)) {
|
|
51
|
+
const wikiFiles = readdirSync(wikiDir).filter(f => f.endsWith('.md'));
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(chalk.gray(` Wiki: ${wikiFiles.length} files`));
|
|
54
|
+
for (const f of wikiFiles.slice(0, 5)) {
|
|
55
|
+
console.log(chalk.gray(' · ') + chalk.white(`.agent/wiki/${f}`));
|
|
56
|
+
}
|
|
57
|
+
if (wikiFiles.length > 5) {
|
|
58
|
+
console.log(chalk.gray(` ... and ${wikiFiles.length - 5} more`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Adapters
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(chalk.gray(' AI tools configured:'));
|
|
65
|
+
const { ADAPTERS } = await import('../adapters/index.js');
|
|
66
|
+
for (const adapterKey of config.adapters) {
|
|
67
|
+
const adapter = ADAPTERS[adapterKey];
|
|
68
|
+
if (!adapter) continue;
|
|
69
|
+
const exists = existsSync(join(cwd, adapter.file));
|
|
70
|
+
console.log(
|
|
71
|
+
' ' + (exists ? chalk.green('✓') : chalk.red('✗')) + ' ' +
|
|
72
|
+
chalk.white(adapter.name) + chalk.gray(` (${adapter.file})`)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { scanProject, buildFileMap } from '../indexer/scanner.js';
|
|
6
|
+
import { ADAPTERS, generateAdapterContent } from '../adapters/index.js';
|
|
7
|
+
|
|
8
|
+
export async function syncCommand(options) {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const configPath = join(cwd, '.agent', 'config.json');
|
|
11
|
+
|
|
12
|
+
if (!existsSync(configPath)) {
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(chalk.yellow(' ⚠ Octboos not initialized.'));
|
|
15
|
+
console.log(chalk.gray(' Run ' + chalk.white('npx octboos init') + ' first.'));
|
|
16
|
+
console.log('');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(chalk.bold.cyan(` Octboos Sync — ${config.projectName}`));
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Scan project
|
|
26
|
+
const spinner = ora({ text: chalk.gray('Scanning project...'), color: 'cyan' }).start();
|
|
27
|
+
const scanResult = await scanProject(cwd);
|
|
28
|
+
spinner.succeed(chalk.green(`Scanned ${scanResult.fileCount} files`));
|
|
29
|
+
|
|
30
|
+
// Update map.md
|
|
31
|
+
const mapSpinner = ora({ text: chalk.gray('Updating map.md...'), color: 'cyan' }).start();
|
|
32
|
+
const mapContent = buildFileMap(scanResult, cwd);
|
|
33
|
+
writeFileSync(join(cwd, '.agent', 'map.md'), mapContent);
|
|
34
|
+
mapSpinner.succeed(chalk.green('Updated .agent/map.md'));
|
|
35
|
+
|
|
36
|
+
// Update adapter files
|
|
37
|
+
const adapterSpinner = ora({ text: chalk.gray('Updating AI tool configs...'), color: 'cyan' }).start();
|
|
38
|
+
for (const adapterKey of config.adapters) {
|
|
39
|
+
const adapter = ADAPTERS[adapterKey];
|
|
40
|
+
if (!adapter) continue;
|
|
41
|
+
const filePath = join(cwd, adapter.file);
|
|
42
|
+
if (existsSync(filePath)) {
|
|
43
|
+
writeFileSync(filePath, generateAdapterContent(adapterKey, config.projectName));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
adapterSpinner.succeed(chalk.green('Updated AI tool configs'));
|
|
47
|
+
|
|
48
|
+
// Update lastSync in config
|
|
49
|
+
config.lastSync = new Date().toISOString();
|
|
50
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(chalk.bold.green(' ✓ Sync complete!'));
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join, extname, relative } from 'path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
// File extensions by language
|
|
7
|
+
const LANG_MAP = {
|
|
8
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript/React',
|
|
9
|
+
'.js': 'JavaScript', '.jsx': 'JavaScript/React',
|
|
10
|
+
'.py': 'Python', '.go': 'Go',
|
|
11
|
+
'.rs': 'Rust', '.java': 'Java',
|
|
12
|
+
'.php': 'PHP', '.rb': 'Ruby',
|
|
13
|
+
'.vue': 'Vue', '.svelte': 'Svelte',
|
|
14
|
+
'.css': 'CSS', '.scss': 'SCSS',
|
|
15
|
+
'.sql': 'SQL', '.prisma': 'Prisma',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Framework detection by file presence
|
|
19
|
+
const FRAMEWORK_SIGNALS = [
|
|
20
|
+
{ file: 'next.config.js', name: 'Next.js' },
|
|
21
|
+
{ file: 'next.config.ts', name: 'Next.js' },
|
|
22
|
+
{ file: 'nuxt.config.ts', name: 'Nuxt.js' },
|
|
23
|
+
{ file: 'vite.config.ts', name: 'Vite' },
|
|
24
|
+
{ file: 'remix.config.js', name: 'Remix' },
|
|
25
|
+
{ file: 'astro.config.mjs', name: 'Astro' },
|
|
26
|
+
{ file: 'svelte.config.js', name: 'SvelteKit' },
|
|
27
|
+
{ file: 'angular.json', name: 'Angular' },
|
|
28
|
+
{ file: 'manage.py', name: 'Django' },
|
|
29
|
+
{ file: 'requirements.txt', name: 'Python' },
|
|
30
|
+
{ file: 'Cargo.toml', name: 'Rust' },
|
|
31
|
+
{ file: 'go.mod', name: 'Go' },
|
|
32
|
+
{ file: 'composer.json', name: 'PHP/Laravel' },
|
|
33
|
+
{ file: 'artisan', name: 'Laravel' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const IGNORE_DIRS = [
|
|
37
|
+
'**/node_modules/**', '**/.git/**', '**/dist/**',
|
|
38
|
+
'**/build/**', '**/.next/**', '**/coverage/**',
|
|
39
|
+
'**/.agent/**', '**/vendor/**', '**/__pycache__/**'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export async function scanProject(cwd = process.cwd()) {
|
|
43
|
+
const result = {
|
|
44
|
+
stack: [],
|
|
45
|
+
frameworks: [],
|
|
46
|
+
languages: {},
|
|
47
|
+
structure: [],
|
|
48
|
+
fileCount: 0,
|
|
49
|
+
hasTests: false,
|
|
50
|
+
hasPrisma: false,
|
|
51
|
+
hasDocker: false,
|
|
52
|
+
packageJson: null,
|
|
53
|
+
gitIgnore: null,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Read package.json if exists
|
|
57
|
+
const pkgPath = join(cwd, 'package.json');
|
|
58
|
+
if (existsSync(pkgPath)) {
|
|
59
|
+
try {
|
|
60
|
+
result.packageJson = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
61
|
+
result.stack.push(...detectFromPackageJson(result.packageJson));
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Detect frameworks by file presence
|
|
66
|
+
for (const signal of FRAMEWORK_SIGNALS) {
|
|
67
|
+
if (existsSync(join(cwd, signal.file))) {
|
|
68
|
+
result.frameworks.push(signal.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Scan all files
|
|
73
|
+
const files = await fg('**/*', {
|
|
74
|
+
cwd,
|
|
75
|
+
ignore: IGNORE_DIRS,
|
|
76
|
+
onlyFiles: true,
|
|
77
|
+
dot: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
result.fileCount = files.length;
|
|
81
|
+
|
|
82
|
+
// Count languages
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const ext = extname(file);
|
|
85
|
+
if (LANG_MAP[ext]) {
|
|
86
|
+
result.languages[LANG_MAP[ext]] = (result.languages[LANG_MAP[ext]] || 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
if (file.includes('.test.') || file.includes('.spec.') || file.includes('__tests__')) {
|
|
89
|
+
result.hasTests = true;
|
|
90
|
+
}
|
|
91
|
+
if (file.endsWith('.prisma')) result.hasPrisma = true;
|
|
92
|
+
if (file === 'Dockerfile' || file === 'docker-compose.yml') result.hasDocker = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build top-level structure
|
|
96
|
+
const topLevel = await fg('*', {
|
|
97
|
+
cwd,
|
|
98
|
+
ignore: IGNORE_DIRS,
|
|
99
|
+
onlyDirectories: true,
|
|
100
|
+
});
|
|
101
|
+
result.structure = topLevel;
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function detectFromPackageJson(pkg) {
|
|
107
|
+
const stack = [];
|
|
108
|
+
const allDeps = {
|
|
109
|
+
...pkg.dependencies || {},
|
|
110
|
+
...pkg.devDependencies || {}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const checks = [
|
|
114
|
+
['react', 'React'],
|
|
115
|
+
['vue', 'Vue'],
|
|
116
|
+
['svelte', 'Svelte'],
|
|
117
|
+
['express', 'Express'],
|
|
118
|
+
['fastify', 'Fastify'],
|
|
119
|
+
['prisma', 'Prisma'],
|
|
120
|
+
['mongoose', 'MongoDB/Mongoose'],
|
|
121
|
+
['typeorm', 'TypeORM'],
|
|
122
|
+
['drizzle-orm', 'Drizzle ORM'],
|
|
123
|
+
['tailwindcss', 'Tailwind CSS'],
|
|
124
|
+
['@mui/material', 'Material UI'],
|
|
125
|
+
['next-auth', 'NextAuth'],
|
|
126
|
+
['@clerk/nextjs', 'Clerk Auth'],
|
|
127
|
+
['zod', 'Zod'],
|
|
128
|
+
['trpc', 'tRPC'],
|
|
129
|
+
['graphql', 'GraphQL'],
|
|
130
|
+
['socket.io', 'Socket.io'],
|
|
131
|
+
['jest', 'Jest'],
|
|
132
|
+
['vitest', 'Vitest'],
|
|
133
|
+
['playwright', 'Playwright'],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
for (const [dep, name] of checks) {
|
|
137
|
+
if (allDeps[dep]) stack.push(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return stack;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildFileMap(scanResult, cwd) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
|
|
146
|
+
lines.push('# Project File Map');
|
|
147
|
+
lines.push(`> Generated by Octboos | ${new Date().toISOString().split('T')[0]}`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
|
|
150
|
+
// Frameworks & Stack
|
|
151
|
+
if (scanResult.frameworks.length > 0) {
|
|
152
|
+
lines.push('## Frameworks');
|
|
153
|
+
for (const f of scanResult.frameworks) lines.push(`- ${f}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (scanResult.stack.length > 0) {
|
|
158
|
+
lines.push('## Stack & Libraries');
|
|
159
|
+
for (const s of scanResult.stack) lines.push(`- ${s}`);
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Languages
|
|
164
|
+
const langs = Object.entries(scanResult.languages)
|
|
165
|
+
.sort((a, b) => b[1] - a[1])
|
|
166
|
+
.slice(0, 5);
|
|
167
|
+
if (langs.length > 0) {
|
|
168
|
+
lines.push('## Languages');
|
|
169
|
+
for (const [lang, count] of langs) {
|
|
170
|
+
lines.push(`- ${lang}: ${count} files`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Directory structure
|
|
176
|
+
if (scanResult.structure.length > 0) {
|
|
177
|
+
lines.push('## Directory Structure');
|
|
178
|
+
for (const dir of scanResult.structure) {
|
|
179
|
+
lines.push(`- \`${dir}/\``);
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Features detected
|
|
185
|
+
const features = [];
|
|
186
|
+
if (scanResult.hasTests) features.push('Tests');
|
|
187
|
+
if (scanResult.hasPrisma) features.push('Prisma ORM');
|
|
188
|
+
if (scanResult.hasDocker) features.push('Docker');
|
|
189
|
+
if (features.length > 0) {
|
|
190
|
+
lines.push('## Detected Features');
|
|
191
|
+
for (const f of features) lines.push(`- ${f}`);
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lines.push(`## Stats`);
|
|
196
|
+
lines.push(`- Total files: ${scanResult.fileCount}`);
|
|
197
|
+
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function detectInstalledTools() {
|
|
202
|
+
const result = {
|
|
203
|
+
detected: [],
|
|
204
|
+
uncertain: [],
|
|
205
|
+
notFound: ['claude', 'opencode', 'cursor', 'aider', 'gemini', 'codex', 'windsurf']
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Tools to check via which/where
|
|
209
|
+
const cliTools = [
|
|
210
|
+
{ name: 'claude', command: 'claude' },
|
|
211
|
+
{ name: 'opencode', command: 'opencode' },
|
|
212
|
+
{ name: 'cursor', command: 'cursor' },
|
|
213
|
+
{ name: 'aider', command: 'aider' },
|
|
214
|
+
{ name: 'gemini', command: 'gemini' },
|
|
215
|
+
{ name: 'codex', command: 'codex' },
|
|
216
|
+
{ name: 'windsurf', command: 'windsurf' }
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
// Check CLI tools
|
|
220
|
+
for (const tool of cliTools) {
|
|
221
|
+
try {
|
|
222
|
+
const platform = process.platform;
|
|
223
|
+
let command;
|
|
224
|
+
if (platform === 'win32') {
|
|
225
|
+
command = `where ${tool.command}`;
|
|
226
|
+
} else {
|
|
227
|
+
command = `which ${tool.command}`;
|
|
228
|
+
}
|
|
229
|
+
execSync(command, { stdio: 'ignore' });
|
|
230
|
+
// Move from notFound to detected
|
|
231
|
+
result.detected.push(tool.name);
|
|
232
|
+
result.notFound = result.notFound.filter(t => t !== tool.name);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// Tool not found via CLI, keep in notFound for now
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check common paths
|
|
239
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
240
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
241
|
+
const appData = process.env.APPDATA;
|
|
242
|
+
|
|
243
|
+
// Mac paths
|
|
244
|
+
const macPaths = [
|
|
245
|
+
{ tool: 'cursor', path: '/Applications/Cursor.app' },
|
|
246
|
+
{ tool: 'windsurf', path: '/Applications/Windsurf.app' },
|
|
247
|
+
{ tool: 'claude', path: `${home}/.claude/` },
|
|
248
|
+
{ tool: 'aider', path: `${home}/.config/aider/` }
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// Windows paths
|
|
252
|
+
const winPaths = [
|
|
253
|
+
{ tool: 'cursor', path: `${localAppData}\\Programs\\cursor` },
|
|
254
|
+
{ tool: 'claude', path: `${appData}\\Claude\\` }
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
const pathsToCheck = process.platform === 'win32' ? winPaths : macPaths;
|
|
258
|
+
|
|
259
|
+
for (const { tool, path } of pathsToCheck) {
|
|
260
|
+
try {
|
|
261
|
+
if (existsSync(path)) {
|
|
262
|
+
// Move from notFound to detected if not already there
|
|
263
|
+
if (!result.detected.includes(tool)) {
|
|
264
|
+
result.detected.push(tool);
|
|
265
|
+
result.notFound = result.notFound.filter(t => t !== tool);
|
|
266
|
+
}
|
|
267
|
+
// If we found it via path but not CLI, mark as uncertain
|
|
268
|
+
if (!result.detected.includes(tool)) {
|
|
269
|
+
result.uncertain.push(tool);
|
|
270
|
+
result.notFound = result.notFound.filter(t => t !== tool);
|
|
271
|
+
result.detected = result.detected.filter(t => t !== tool);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// Path doesn't exist or other error
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Remove duplicates
|
|
280
|
+
result.detected = [...new Set(result.detected)];
|
|
281
|
+
result.uncertain = [...new Set(result.uncertain)];
|
|
282
|
+
result.notFound = [...new Set(result.notFound)];
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|