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 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
+ }
@@ -0,0 +1,2 @@
1
+ import type { IdeAgentsConfig, ApplyResult } from "./types.js";
2
+ export declare function applyInstallations(config: IdeAgentsConfig): Promise<ApplyResult>;
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -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
+ }
@@ -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;