ide-agents 0.1.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 +131 -0
- package/dist/adapters/cursor.d.ts +10 -0
- package/dist/adapters/cursor.js +39 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +82 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +86 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +173 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +57 -0
- package/dist/scan.d.ts +2 -0
- package/dist/scan.js +112 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +195 -0
- package/dist/targets.d.ts +4 -0
- package/dist/targets.js +42 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.js +1 -0
- package/package.json +77 -0
- package/web/dist/assets/index-D7KhBlEO.js +124 -0
- package/web/dist/assets/index-JihkyerF.css +1 -0
- package/web/dist/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ide-agents contributors
|
|
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,131 @@
|
|
|
1
|
+
# ide-agents
|
|
2
|
+
|
|
3
|
+
Local admin for **IDE agents and skills** (Cursor and similar) from any git repository.
|
|
4
|
+
|
|
5
|
+
Install skills and subagents into your IDE via symlinks — no copy-paste, no manual path juggling.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Clones git repositories into `~/.ide-agents/repos/`
|
|
10
|
+
- Scans `skills/*/SKILL.md` and optional `agents/*.md`
|
|
11
|
+
- Creates symlinks in `~/.cursor/` (global) or `<project>/.cursor/` (per-project)
|
|
12
|
+
- Provides a browser UI for repos, skills, and agents
|
|
13
|
+
|
|
14
|
+
**Not affiliated with Cursor.**
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- **macOS** or **Linux** (Windows is not supported in v0.1)
|
|
19
|
+
- **Node.js 20+**
|
|
20
|
+
- **git** in PATH
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i -g ide-agents
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or from source:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/sergeychernov/agentdesk.git
|
|
32
|
+
cd agentdesk
|
|
33
|
+
npm install
|
|
34
|
+
npm run build
|
|
35
|
+
npm i -g .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ide-agents
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This will:
|
|
45
|
+
|
|
46
|
+
1. Create `~/.ide-agents/` on first run (migrates from `~/.agentdesk/` if present)
|
|
47
|
+
2. Start a local server at `http://127.0.0.1:3921` (or the next free port)
|
|
48
|
+
3. Open the UI in your browser
|
|
49
|
+
|
|
50
|
+
Press `Ctrl+C` to stop.
|
|
51
|
+
|
|
52
|
+
### CLI options
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ide-agents --port 3922 # custom port
|
|
56
|
+
ide-agents --no-open # do not open browser
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Add a repository
|
|
60
|
+
|
|
61
|
+
1. Open **Settings** in the UI
|
|
62
|
+
2. Enter a git URL and branch (default `main`)
|
|
63
|
+
3. Click **Add / Clone**
|
|
64
|
+
|
|
65
|
+
Your repo should contain:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
skills/
|
|
69
|
+
my-skill/
|
|
70
|
+
SKILL.md
|
|
71
|
+
agents/ # optional
|
|
72
|
+
my-agent.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For local testing, use a `file://` URL:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
file:///Users/you/code/my-skills-repo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Private repos: configure SSH or `gh` auth yourself — ide-agents does not store tokens.
|
|
82
|
+
|
|
83
|
+
## Install artifacts
|
|
84
|
+
|
|
85
|
+
1. Go to **Skills** or **Agents**
|
|
86
|
+
2. Select a repository
|
|
87
|
+
3. Click **Global** (🌐) or **Project** (📁) on a card — symlinks apply immediately
|
|
88
|
+
|
|
89
|
+
| Kind | Global | Project |
|
|
90
|
+
|-------|--------------------------------|--------------------------------------|
|
|
91
|
+
| Skill | `~/.cursor/skills/<name>` | `<project>/.cursor/skills/<name>` |
|
|
92
|
+
| Agent | `~/.cursor/agents/<name>.md` | `<project>/.cursor/agents/<name>.md` |
|
|
93
|
+
|
|
94
|
+
Click the active icon again to remove the symlink (only if target is already a symlink).
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install
|
|
100
|
+
npm run dev # server on :3921 + Vite on :5173
|
|
101
|
+
npm run build # compile server + web
|
|
102
|
+
npm start # run production build
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Documentation site (Docusaurus):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run docs:install
|
|
109
|
+
npm run docs:start # http://localhost:3000
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Fixture repo for manual testing:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd fixtures/sample-repo && git init && git add . && git commit -m "init"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then add `file://$(pwd)` in the UI.
|
|
119
|
+
|
|
120
|
+
## Data layout
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
~/.ide-agents/
|
|
124
|
+
├── config.json
|
|
125
|
+
└── repos/
|
|
126
|
+
└── <slug>/ # git clone
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArtifactKind, Installation } from "../types.js";
|
|
2
|
+
export interface Adapter {
|
|
3
|
+
id: "cursor";
|
|
4
|
+
getGlobalTargetPath(installation: Pick<Installation, "kind" | "targetName">): string;
|
|
5
|
+
getProjectTargetPath(installation: Pick<Installation, "kind" | "targetName" | "projectPath">): string;
|
|
6
|
+
getSourcePath(repoRoot: string, installation: Pick<Installation, "sourcePath">): string;
|
|
7
|
+
}
|
|
8
|
+
export declare const cursorAdapter: Adapter;
|
|
9
|
+
export declare function getAdapter(adapterId: string): Adapter;
|
|
10
|
+
export declare function isSymlinkType(kind: ArtifactKind): "dir" | "file";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolveProjectPath } from "../paths.js";
|
|
4
|
+
function skillsDir(base) {
|
|
5
|
+
return path.join(base, ".cursor", "skills");
|
|
6
|
+
}
|
|
7
|
+
function agentsDir(base) {
|
|
8
|
+
return path.join(base, ".cursor", "agents");
|
|
9
|
+
}
|
|
10
|
+
function targetPath(base, kind, targetName) {
|
|
11
|
+
if (kind === "skill") {
|
|
12
|
+
return path.join(skillsDir(base), targetName);
|
|
13
|
+
}
|
|
14
|
+
return path.join(agentsDir(base), `${targetName}.md`);
|
|
15
|
+
}
|
|
16
|
+
export const cursorAdapter = {
|
|
17
|
+
id: "cursor",
|
|
18
|
+
getGlobalTargetPath(installation) {
|
|
19
|
+
return targetPath(homedir(), installation.kind, installation.targetName);
|
|
20
|
+
},
|
|
21
|
+
getProjectTargetPath(installation) {
|
|
22
|
+
if (!installation.projectPath) {
|
|
23
|
+
throw new Error("projectPath is required for project target");
|
|
24
|
+
}
|
|
25
|
+
return targetPath(resolveProjectPath(installation.projectPath), installation.kind, installation.targetName);
|
|
26
|
+
},
|
|
27
|
+
getSourcePath(repoRoot, installation) {
|
|
28
|
+
return path.join(repoRoot, installation.sourcePath);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function getAdapter(adapterId) {
|
|
32
|
+
if (adapterId === "cursor") {
|
|
33
|
+
return cursorAdapter;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Unknown adapter: ${adapterId}`);
|
|
36
|
+
}
|
|
37
|
+
export function isSymlinkType(kind) {
|
|
38
|
+
return kind === "skill" ? "dir" : "file";
|
|
39
|
+
}
|
package/dist/apply.d.ts
ADDED
package/dist/apply.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { lstat, mkdir, readlink, symlink, unlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getAdapter } from "./adapters/cursor.js";
|
|
4
|
+
import { getRepoPath } from "./paths.js";
|
|
5
|
+
async function pathExists(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
await lstat(filePath);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function ensureParentDir(filePath) {
|
|
15
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
async function removeSymlinkIfExists(targetPath) {
|
|
18
|
+
if (!(await pathExists(targetPath))) {
|
|
19
|
+
return { path: targetPath, action: "skipped" };
|
|
20
|
+
}
|
|
21
|
+
const stats = await lstat(targetPath);
|
|
22
|
+
if (!stats.isSymbolicLink()) {
|
|
23
|
+
return {
|
|
24
|
+
path: targetPath,
|
|
25
|
+
action: "skipped",
|
|
26
|
+
error: "Target exists and is not a symlink",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
await unlink(targetPath);
|
|
30
|
+
return { path: targetPath, action: "removed" };
|
|
31
|
+
}
|
|
32
|
+
async function createSymlink(targetPath, sourcePath, type) {
|
|
33
|
+
const resolvedSource = path.resolve(sourcePath);
|
|
34
|
+
if (await pathExists(targetPath)) {
|
|
35
|
+
const stats = await lstat(targetPath);
|
|
36
|
+
if (stats.isSymbolicLink()) {
|
|
37
|
+
const current = await readlink(targetPath);
|
|
38
|
+
if (path.resolve(path.dirname(targetPath), current) === resolvedSource) {
|
|
39
|
+
return { path: targetPath, action: "skipped" };
|
|
40
|
+
}
|
|
41
|
+
await unlink(targetPath);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return {
|
|
45
|
+
path: targetPath,
|
|
46
|
+
action: "skipped",
|
|
47
|
+
error: "Target exists and is not a symlink",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await ensureParentDir(targetPath);
|
|
52
|
+
await symlink(resolvedSource, targetPath, type);
|
|
53
|
+
return { path: targetPath, action: "created" };
|
|
54
|
+
}
|
|
55
|
+
async function applyScope(installation, sourcePath, type, enabled, targetPath) {
|
|
56
|
+
const results = [];
|
|
57
|
+
if (enabled) {
|
|
58
|
+
try {
|
|
59
|
+
results.push(await createSymlink(targetPath, sourcePath, type));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
results.push({
|
|
63
|
+
path: targetPath,
|
|
64
|
+
action: "skipped",
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
const removed = await removeSymlinkIfExists(targetPath);
|
|
71
|
+
if (removed.action === "removed") {
|
|
72
|
+
results.push(removed);
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
export async function applyInstallations(config) {
|
|
77
|
+
const adapter = getAdapter(config.adapter);
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const installation of config.installations) {
|
|
80
|
+
const repo = config.repos.find((r) => r.id === installation.repoId);
|
|
81
|
+
if (!repo) {
|
|
82
|
+
results.push({
|
|
83
|
+
path: installation.targetName,
|
|
84
|
+
action: "skipped",
|
|
85
|
+
error: `Unknown repo: ${installation.repoId}`,
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const repoRoot = getRepoPath(repo.slug);
|
|
90
|
+
const sourcePath = adapter.getSourcePath(repoRoot, installation);
|
|
91
|
+
const type = installation.kind === "skill" ? "dir" : "file";
|
|
92
|
+
const globalTarget = adapter.getGlobalTargetPath(installation);
|
|
93
|
+
results.push(...(await applyScope(installation, sourcePath, type, installation.global, globalTarget)));
|
|
94
|
+
if (installation.projectPath) {
|
|
95
|
+
const projectTarget = adapter.getProjectTargetPath(installation);
|
|
96
|
+
results.push(...(await applyScope(installation, sourcePath, type, installation.project, projectTarget)));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { results };
|
|
100
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readConfig } from "./config.js";
|
|
6
|
+
import { startServer } from "./server.js";
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const options = {};
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === "--no-open") {
|
|
12
|
+
options.noOpen = true;
|
|
13
|
+
}
|
|
14
|
+
else if (arg === "--port" && argv[i + 1]) {
|
|
15
|
+
options.port = Number.parseInt(argv[i + 1], 10);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
else if (arg === "ui") {
|
|
19
|
+
// default command
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return options;
|
|
23
|
+
}
|
|
24
|
+
async function isPortAvailable(port, host) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const server = net.createServer();
|
|
27
|
+
server.once("error", () => resolve(false));
|
|
28
|
+
server.once("listening", () => {
|
|
29
|
+
server.close(() => resolve(true));
|
|
30
|
+
});
|
|
31
|
+
server.listen(port, host);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function findAvailablePort(startPort, host, maxAttempts = 10) {
|
|
35
|
+
for (let offset = 0; offset < maxAttempts; offset++) {
|
|
36
|
+
const port = startPort + offset;
|
|
37
|
+
if (await isPortAvailable(port, host)) {
|
|
38
|
+
return port;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Ports ${startPort}-${startPort + maxAttempts - 1} are busy on ${host}`);
|
|
42
|
+
}
|
|
43
|
+
function openBrowser(url) {
|
|
44
|
+
const platform = process.platform;
|
|
45
|
+
if (platform === "darwin") {
|
|
46
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
47
|
+
}
|
|
48
|
+
else if (platform === "linux") {
|
|
49
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(`Open ${url} in your browser`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function main() {
|
|
56
|
+
const options = parseArgs(process.argv.slice(2));
|
|
57
|
+
const config = await readConfig();
|
|
58
|
+
const host = "127.0.0.1";
|
|
59
|
+
const requestedPort = options.port ?? config.server.port;
|
|
60
|
+
const port = await findAvailablePort(requestedPort, host);
|
|
61
|
+
const url = `http://${host}:${port}`;
|
|
62
|
+
const app = await startServer({
|
|
63
|
+
port,
|
|
64
|
+
host,
|
|
65
|
+
launchCwd: path.resolve(process.cwd()),
|
|
66
|
+
});
|
|
67
|
+
console.log(`ide-agents running at ${url}`);
|
|
68
|
+
if (!options.noOpen) {
|
|
69
|
+
openBrowser(url);
|
|
70
|
+
}
|
|
71
|
+
const shutdown = async () => {
|
|
72
|
+
console.log("\nShutting down...");
|
|
73
|
+
await app.close();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
};
|
|
76
|
+
process.on("SIGINT", shutdown);
|
|
77
|
+
process.on("SIGTERM", shutdown);
|
|
78
|
+
}
|
|
79
|
+
main().catch((err) => {
|
|
80
|
+
console.error(err instanceof Error ? err.message : err);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IdeAgentsConfig } from "./types.js";
|
|
2
|
+
export declare function ensureIdeAgentsHome(): Promise<string>;
|
|
3
|
+
export declare function readConfig(): Promise<IdeAgentsConfig>;
|
|
4
|
+
export declare function writeConfig(config: IdeAgentsConfig): Promise<void>;
|
|
5
|
+
export declare function addRecentProject(config: IdeAgentsConfig, projectPath: string): Promise<IdeAgentsConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { getConfigPath, getIdeAgentsHome } from "./paths.js";
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
version: 1,
|
|
7
|
+
adapter: "cursor",
|
|
8
|
+
server: { port: 3921 },
|
|
9
|
+
repos: [],
|
|
10
|
+
installations: [],
|
|
11
|
+
recentProjects: [],
|
|
12
|
+
};
|
|
13
|
+
const LEGACY_HOME = path.join(homedir(), ".agentdesk");
|
|
14
|
+
async function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
await access(filePath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function migrateInstallation(raw) {
|
|
24
|
+
const item = raw;
|
|
25
|
+
if (typeof item.global === "boolean") {
|
|
26
|
+
return item;
|
|
27
|
+
}
|
|
28
|
+
const scope = item.scope;
|
|
29
|
+
return {
|
|
30
|
+
id: item.id,
|
|
31
|
+
repoId: item.repoId,
|
|
32
|
+
kind: item.kind,
|
|
33
|
+
artifactId: item.artifactId,
|
|
34
|
+
sourcePath: item.sourcePath,
|
|
35
|
+
targetName: item.targetName,
|
|
36
|
+
global: scope === "global",
|
|
37
|
+
project: scope === "project",
|
|
38
|
+
projectPath: item.projectPath ?? null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function migrateLegacyHome() {
|
|
42
|
+
const home = getIdeAgentsHome();
|
|
43
|
+
if (home === LEGACY_HOME) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!(await fileExists(home)) && (await fileExists(LEGACY_HOME))) {
|
|
47
|
+
await rename(LEGACY_HOME, home);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function ensureIdeAgentsHome() {
|
|
51
|
+
await migrateLegacyHome();
|
|
52
|
+
const home = getIdeAgentsHome();
|
|
53
|
+
await mkdir(home, { recursive: true });
|
|
54
|
+
await mkdir(path.join(home, "repos"), { recursive: true });
|
|
55
|
+
return home;
|
|
56
|
+
}
|
|
57
|
+
export async function readConfig() {
|
|
58
|
+
await ensureIdeAgentsHome();
|
|
59
|
+
const configPath = getConfigPath();
|
|
60
|
+
if (!(await fileExists(configPath))) {
|
|
61
|
+
await writeConfig(DEFAULT_CONFIG);
|
|
62
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
63
|
+
}
|
|
64
|
+
const raw = await readFile(configPath, "utf8");
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
return {
|
|
67
|
+
...DEFAULT_CONFIG,
|
|
68
|
+
...parsed,
|
|
69
|
+
server: { ...DEFAULT_CONFIG.server, ...parsed.server },
|
|
70
|
+
repos: parsed.repos ?? [],
|
|
71
|
+
installations: (parsed.installations ?? []).map(migrateInstallation),
|
|
72
|
+
recentProjects: parsed.recentProjects ?? [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function writeConfig(config) {
|
|
76
|
+
await ensureIdeAgentsHome();
|
|
77
|
+
await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
78
|
+
}
|
|
79
|
+
export async function addRecentProject(config, projectPath) {
|
|
80
|
+
const resolved = path.resolve(projectPath);
|
|
81
|
+
const recent = [
|
|
82
|
+
resolved,
|
|
83
|
+
...config.recentProjects.filter((p) => p !== resolved),
|
|
84
|
+
].slice(0, 10);
|
|
85
|
+
return { ...config, recentProjects: recent };
|
|
86
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { GitStatus } from "./types.js";
|
|
2
|
+
export declare function cloneRepo(url: string, slug: string, ref: string): Promise<string>;
|
|
3
|
+
export declare function fetchRepo(slug: string): Promise<void>;
|
|
4
|
+
export declare function pullRepo(slug: string): Promise<void>;
|
|
5
|
+
export declare function getGitStatus(slug: string, ref: string): Promise<GitStatus>;
|
|
6
|
+
export declare function getGitStatusWithoutFetch(slug: string, ref: string): Promise<GitStatus>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
5
|
+
import { getRepoPath } from "./paths.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
async function isGitRepo(dir) {
|
|
8
|
+
try {
|
|
9
|
+
await access(path.join(dir, ".git"));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function runGit(cwd, args) {
|
|
17
|
+
const result = await execFileAsync("git", args, {
|
|
18
|
+
cwd,
|
|
19
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
20
|
+
});
|
|
21
|
+
return { stdout: result.stdout.trim(), stderr: result.stderr.trim() };
|
|
22
|
+
}
|
|
23
|
+
export async function cloneRepo(url, slug, ref) {
|
|
24
|
+
const target = getRepoPath(slug);
|
|
25
|
+
if (await isGitRepo(target)) {
|
|
26
|
+
throw new Error(`Repository already cloned at ${target}`);
|
|
27
|
+
}
|
|
28
|
+
await execFileAsync("git", ["clone", "--branch", ref, url, target], {
|
|
29
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
30
|
+
}).catch(async (err) => {
|
|
31
|
+
// Retry without --branch for repos where ref is not a branch at clone time
|
|
32
|
+
try {
|
|
33
|
+
await execFileAsync("git", ["clone", url, target], {
|
|
34
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
await runGit(target, ["checkout", ref]);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(err.stderr?.trim() || err.message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
export async function fetchRepo(slug) {
|
|
45
|
+
const cwd = getRepoPath(slug);
|
|
46
|
+
if (!(await isGitRepo(cwd))) {
|
|
47
|
+
throw new Error(`Not a git repository: ${cwd}`);
|
|
48
|
+
}
|
|
49
|
+
await runGit(cwd, ["fetch", "--all", "--prune"]);
|
|
50
|
+
}
|
|
51
|
+
export async function pullRepo(slug) {
|
|
52
|
+
const cwd = getRepoPath(slug);
|
|
53
|
+
if (!(await isGitRepo(cwd))) {
|
|
54
|
+
throw new Error(`Not a git repository: ${cwd}`);
|
|
55
|
+
}
|
|
56
|
+
await runGit(cwd, ["pull", "--ff-only"]);
|
|
57
|
+
}
|
|
58
|
+
export async function getGitStatus(slug, ref) {
|
|
59
|
+
const cwd = getRepoPath(slug);
|
|
60
|
+
if (!(await isGitRepo(cwd))) {
|
|
61
|
+
return {
|
|
62
|
+
branch: null,
|
|
63
|
+
sha: null,
|
|
64
|
+
dirty: false,
|
|
65
|
+
behind: null,
|
|
66
|
+
ahead: null,
|
|
67
|
+
error: "Repository not cloned",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const branchResult = await runGit(cwd, [
|
|
72
|
+
"rev-parse",
|
|
73
|
+
"--abbrev-ref",
|
|
74
|
+
"HEAD",
|
|
75
|
+
]);
|
|
76
|
+
const shaResult = await runGit(cwd, ["rev-parse", "HEAD"]);
|
|
77
|
+
const dirtyResult = await runGit(cwd, ["status", "--porcelain"]);
|
|
78
|
+
let behind = null;
|
|
79
|
+
let ahead = null;
|
|
80
|
+
try {
|
|
81
|
+
await runGit(cwd, ["fetch", "--quiet"]);
|
|
82
|
+
const revList = await runGit(cwd, [
|
|
83
|
+
"rev-list",
|
|
84
|
+
"--left-right",
|
|
85
|
+
"--count",
|
|
86
|
+
`HEAD...origin/${ref}`,
|
|
87
|
+
]);
|
|
88
|
+
const [aheadStr, behindStr] = revList.stdout.split(/\s+/);
|
|
89
|
+
ahead = Number.parseInt(aheadStr ?? "0", 10);
|
|
90
|
+
behind = Number.parseInt(behindStr ?? "0", 10);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Remote tracking may not exist yet
|
|
94
|
+
behind = null;
|
|
95
|
+
ahead = null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
branch: branchResult.stdout || null,
|
|
99
|
+
sha: shaResult.stdout || null,
|
|
100
|
+
dirty: dirtyResult.stdout.length > 0,
|
|
101
|
+
behind,
|
|
102
|
+
ahead,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
return {
|
|
108
|
+
branch: null,
|
|
109
|
+
sha: null,
|
|
110
|
+
dirty: false,
|
|
111
|
+
behind: null,
|
|
112
|
+
ahead: null,
|
|
113
|
+
error: message,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function getGitStatusWithoutFetch(slug, ref) {
|
|
118
|
+
const cwd = getRepoPath(slug);
|
|
119
|
+
if (!(await isGitRepo(cwd))) {
|
|
120
|
+
return {
|
|
121
|
+
branch: null,
|
|
122
|
+
sha: null,
|
|
123
|
+
dirty: false,
|
|
124
|
+
behind: null,
|
|
125
|
+
ahead: null,
|
|
126
|
+
error: "Repository not cloned",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const branchResult = await runGit(cwd, [
|
|
131
|
+
"rev-parse",
|
|
132
|
+
"--abbrev-ref",
|
|
133
|
+
"HEAD",
|
|
134
|
+
]);
|
|
135
|
+
const shaResult = await runGit(cwd, ["rev-parse", "HEAD"]);
|
|
136
|
+
const dirtyResult = await runGit(cwd, ["status", "--porcelain"]);
|
|
137
|
+
let behind = null;
|
|
138
|
+
let ahead = null;
|
|
139
|
+
try {
|
|
140
|
+
const revList = await runGit(cwd, [
|
|
141
|
+
"rev-list",
|
|
142
|
+
"--left-right",
|
|
143
|
+
"--count",
|
|
144
|
+
`HEAD...origin/${ref}`,
|
|
145
|
+
]);
|
|
146
|
+
const [aheadStr, behindStr] = revList.stdout.split(/\s+/);
|
|
147
|
+
ahead = Number.parseInt(aheadStr ?? "0", 10);
|
|
148
|
+
behind = Number.parseInt(behindStr ?? "0", 10);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
behind = null;
|
|
152
|
+
ahead = null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
branch: branchResult.stdout || null,
|
|
156
|
+
sha: shaResult.stdout || null,
|
|
157
|
+
dirty: dirtyResult.stdout.length > 0,
|
|
158
|
+
behind,
|
|
159
|
+
ahead,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
return {
|
|
165
|
+
branch: null,
|
|
166
|
+
sha: null,
|
|
167
|
+
dirty: false,
|
|
168
|
+
behind: null,
|
|
169
|
+
ahead: null,
|
|
170
|
+
error: message,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getIdeAgentsHome(): string;
|
|
2
|
+
export declare function getConfigPath(): string;
|
|
3
|
+
export declare function getStatePath(): string;
|
|
4
|
+
export declare function getReposDir(): string;
|
|
5
|
+
export declare function getRepoPath(slug: string): string;
|
|
6
|
+
export declare function slugFromUrl(url: string): string;
|
|
7
|
+
export declare function getWebDistDir(): string;
|
|
8
|
+
export declare function resolveProjectPath(projectPath: string): string;
|