ide-agents 0.1.1 → 0.2.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/README.md +21 -10
- package/dist/adapters/create.d.ts +9 -0
- package/dist/adapters/create.js +33 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/types.d.ts +7 -0
- package/dist/adapters/types.js +1 -0
- package/dist/apply.js +38 -11
- package/dist/config.js +48 -13
- package/dist/git.js +17 -1
- package/dist/gitignore.d.ts +5 -0
- package/dist/gitignore.js +105 -0
- package/dist/ides.d.ts +5 -0
- package/dist/ides.js +52 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +10 -0
- package/dist/scan.js +58 -29
- package/dist/server.js +72 -5
- package/dist/targets.d.ts +2 -1
- package/dist/targets.js +24 -6
- package/dist/types.d.ts +16 -2
- package/dist/types.js +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/package.json +8 -8
- package/web/dist/assets/index-BSBPsDYY.css +1 -0
- package/web/dist/assets/index-C1Mi5f8W.js +51 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D7KhBlEO.js +0 -124
- package/web/dist/assets/index-JihkyerF.css +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ide-agents
|
|
2
2
|
|
|
3
|
-
Local admin for **IDE agents and skills**
|
|
3
|
+
Local admin for **IDE agents and skills** from any git repository — **Cursor**, **Claude Code**, and **Codex** (enable in Settings).
|
|
4
4
|
|
|
5
5
|
Install skills and subagents into your IDE via symlinks — no copy-paste, no manual path juggling.
|
|
6
6
|
|
|
@@ -11,8 +11,8 @@ Install skills and subagents into your IDE via symlinks — no copy-paste, no ma
|
|
|
11
11
|
|
|
12
12
|
- Clones git repositories into `~/.ide-agents/repos/`
|
|
13
13
|
- Scans `skills/*/SKILL.md` and optional `agents/*.md`
|
|
14
|
-
- Creates symlinks in
|
|
15
|
-
- Provides a browser UI
|
|
14
|
+
- Creates symlinks in each enabled tool’s config directory and per-project folders
|
|
15
|
+
- Provides a browser UI: Settings, Repositories, Skills, Agents
|
|
16
16
|
|
|
17
17
|
**Not affiliated with Cursor.**
|
|
18
18
|
|
|
@@ -59,10 +59,16 @@ ide-agents --port 3922 # custom port
|
|
|
59
59
|
ide-agents --no-open # do not open browser
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
## Settings
|
|
63
|
+
|
|
64
|
+
Open **Settings** (`/settings`) and enable the tools you use (Codex, Claude, Cursor). Set each **config path** (defaults: `~/.codex`, `~/.claude`, `~/.cursor`).
|
|
65
|
+
|
|
66
|
+
On first run, a tool is enabled only if its default folder already exists in your home directory. You can change paths and toggles anytime; installs apply to all enabled tools.
|
|
67
|
+
|
|
62
68
|
## Add a repository
|
|
63
69
|
|
|
64
|
-
1. Open **
|
|
65
|
-
2.
|
|
70
|
+
1. Open **Repositories** in the UI
|
|
71
|
+
2. Pick a suggested catalog or enter a git URL and branch (default `main`)
|
|
66
72
|
3. Click **Add / Clone**
|
|
67
73
|
|
|
68
74
|
Your repo should contain:
|
|
@@ -89,10 +95,13 @@ Private repos: configure SSH or `gh` auth yourself — ide-agents does not store
|
|
|
89
95
|
2. Select a repository
|
|
90
96
|
3. Click **Global** (🌐) or **Project** (📁) on a card — symlinks apply immediately
|
|
91
97
|
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
|
98
|
+
| Tool | Global (default config path) | Project subfolder |
|
|
99
|
+
|--------|------------------------------|-------------------|
|
|
100
|
+
| Cursor | `~/.cursor/` | `.cursor` |
|
|
101
|
+
| Claude | `~/.claude/` | `.claude` |
|
|
102
|
+
| Codex | `~/.codex/` | `.agents` |
|
|
103
|
+
|
|
104
|
+
Global paths use your configured **config path** per tool. Project path is the directory where you started `ide-agents`.
|
|
96
105
|
|
|
97
106
|
Click the active icon again to remove the symlink (only if target is already a symlink).
|
|
98
107
|
|
|
@@ -124,11 +133,13 @@ Then add `file://$(pwd)` in the UI.
|
|
|
124
133
|
|
|
125
134
|
```
|
|
126
135
|
~/.ide-agents/
|
|
127
|
-
├── config.json
|
|
136
|
+
├── config.json # repos, installations, ides (per-tool enable + paths)
|
|
128
137
|
└── repos/
|
|
129
138
|
└── <slug>/ # git clone
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
Docs: [Settings & IDEs](https://ide-agents.vercel.app/docs/settings) on the project site.
|
|
142
|
+
|
|
132
143
|
## License
|
|
133
144
|
|
|
134
145
|
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IdeId } from "../types.js";
|
|
2
|
+
import type { Adapter } from "./types.js";
|
|
3
|
+
interface IdeLayout {
|
|
4
|
+
id: IdeId;
|
|
5
|
+
/** Subfolder under a project root for project-scoped installs. */
|
|
6
|
+
projectDir: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createAdapter(layout: IdeLayout, configPath: string): Adapter;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { expandUserPath, resolveProjectPath } from "../paths.js";
|
|
3
|
+
function skillsDir(base) {
|
|
4
|
+
return path.join(base, "skills");
|
|
5
|
+
}
|
|
6
|
+
function agentsDir(base) {
|
|
7
|
+
return path.join(base, "agents");
|
|
8
|
+
}
|
|
9
|
+
function targetPath(base, kind, targetName) {
|
|
10
|
+
if (kind === "skill") {
|
|
11
|
+
return path.join(skillsDir(base), targetName);
|
|
12
|
+
}
|
|
13
|
+
return path.join(agentsDir(base), `${targetName}.md`);
|
|
14
|
+
}
|
|
15
|
+
export function createAdapter(layout, configPath) {
|
|
16
|
+
const globalBase = expandUserPath(configPath);
|
|
17
|
+
return {
|
|
18
|
+
id: layout.id,
|
|
19
|
+
getGlobalTargetPath(installation) {
|
|
20
|
+
return targetPath(globalBase, installation.kind, installation.targetName);
|
|
21
|
+
},
|
|
22
|
+
getProjectTargetPath(installation) {
|
|
23
|
+
if (!installation.projectPath) {
|
|
24
|
+
throw new Error("projectPath is required for project target");
|
|
25
|
+
}
|
|
26
|
+
const projectBase = path.join(resolveProjectPath(installation.projectPath), layout.projectDir);
|
|
27
|
+
return targetPath(projectBase, installation.kind, installation.targetName);
|
|
28
|
+
},
|
|
29
|
+
getSourcePath(repoRoot, installation) {
|
|
30
|
+
return path.join(repoRoot, installation.sourcePath);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IdeAgentsConfig, IdeId } from "../types.js";
|
|
2
|
+
import type { Adapter } from "./types.js";
|
|
3
|
+
export declare function getAdapter(ideId: IdeId, configPath: string): Adapter;
|
|
4
|
+
export declare function getEnabledAdapters(config: IdeAgentsConfig): Adapter[];
|
|
5
|
+
export declare function isSymlinkType(kind: "skill" | "agent"): "dir" | "file";
|
|
6
|
+
export type { Adapter } from "./types.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getEnabledIdeIds } from "../ides.js";
|
|
2
|
+
import { createAdapter } from "./create.js";
|
|
3
|
+
const LAYOUTS = {
|
|
4
|
+
cursor: { id: "cursor", projectDir: ".cursor" },
|
|
5
|
+
claude: { id: "claude", projectDir: ".claude" },
|
|
6
|
+
codex: { id: "codex", projectDir: ".agents" },
|
|
7
|
+
};
|
|
8
|
+
export function getAdapter(ideId, configPath) {
|
|
9
|
+
return createAdapter(LAYOUTS[ideId], configPath);
|
|
10
|
+
}
|
|
11
|
+
export function getEnabledAdapters(config) {
|
|
12
|
+
return getEnabledIdeIds(config).map((id) => getAdapter(id, config.ides[id].configPath));
|
|
13
|
+
}
|
|
14
|
+
export function isSymlinkType(kind) {
|
|
15
|
+
return kind === "skill" ? "dir" : "file";
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IdeId, Installation } from "../types.js";
|
|
2
|
+
export interface Adapter {
|
|
3
|
+
id: IdeId;
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/apply.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { lstat, mkdir, readlink, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getEnabledAdapters } from "./adapters/index.js";
|
|
4
|
+
import { addManagedGitignoreEntry, removeManagedGitignoreEntry, toGitignorePath, } from "./gitignore.js";
|
|
5
|
+
import { getRepoPath, resolveProjectPath } from "./paths.js";
|
|
5
6
|
async function pathExists(filePath) {
|
|
6
7
|
try {
|
|
7
8
|
await lstat(filePath);
|
|
@@ -52,11 +53,29 @@ async function createSymlink(targetPath, sourcePath, type) {
|
|
|
52
53
|
await symlink(resolvedSource, targetPath, type);
|
|
53
54
|
return { path: targetPath, action: "created" };
|
|
54
55
|
}
|
|
55
|
-
async function
|
|
56
|
+
async function syncProjectGitignore(projectRoot, targetPath, mode) {
|
|
57
|
+
if (!projectRoot) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const entry = toGitignorePath(projectRoot, targetPath);
|
|
61
|
+
if (!entry) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (mode === "add") {
|
|
65
|
+
await addManagedGitignoreEntry(projectRoot, entry);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await removeManagedGitignoreEntry(projectRoot, entry);
|
|
69
|
+
}
|
|
70
|
+
async function applyScope(sourcePath, type, enabled, targetPath, projectRoot) {
|
|
56
71
|
const results = [];
|
|
57
72
|
if (enabled) {
|
|
58
73
|
try {
|
|
59
|
-
|
|
74
|
+
const result = await createSymlink(targetPath, sourcePath, type);
|
|
75
|
+
results.push(result);
|
|
76
|
+
if (!result.error) {
|
|
77
|
+
await syncProjectGitignore(projectRoot, targetPath, "add");
|
|
78
|
+
}
|
|
60
79
|
}
|
|
61
80
|
catch (err) {
|
|
62
81
|
results.push({
|
|
@@ -70,12 +89,16 @@ async function applyScope(installation, sourcePath, type, enabled, targetPath) {
|
|
|
70
89
|
const removed = await removeSymlinkIfExists(targetPath);
|
|
71
90
|
if (removed.action === "removed") {
|
|
72
91
|
results.push(removed);
|
|
92
|
+
await syncProjectGitignore(projectRoot, targetPath, "remove");
|
|
73
93
|
}
|
|
74
94
|
return results;
|
|
75
95
|
}
|
|
76
96
|
export async function applyInstallations(config) {
|
|
77
|
-
const
|
|
97
|
+
const adapters = getEnabledAdapters(config);
|
|
78
98
|
const results = [];
|
|
99
|
+
if (adapters.length === 0) {
|
|
100
|
+
return { results };
|
|
101
|
+
}
|
|
79
102
|
for (const installation of config.installations) {
|
|
80
103
|
const repo = config.repos.find((r) => r.id === installation.repoId);
|
|
81
104
|
if (!repo) {
|
|
@@ -87,13 +110,17 @@ export async function applyInstallations(config) {
|
|
|
87
110
|
continue;
|
|
88
111
|
}
|
|
89
112
|
const repoRoot = getRepoPath(repo.slug);
|
|
90
|
-
const sourcePath = adapter.getSourcePath(repoRoot, installation);
|
|
91
113
|
const type = installation.kind === "skill" ? "dir" : "file";
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
for (const adapter of adapters) {
|
|
115
|
+
const sourcePath = adapter.getSourcePath(repoRoot, installation);
|
|
116
|
+
const globalTarget = adapter.getGlobalTargetPath(installation);
|
|
117
|
+
results.push(...(await applyScope(sourcePath, type, installation.global, globalTarget, null)));
|
|
118
|
+
// projectPath is kept in config while project is off so removal can run.
|
|
119
|
+
if (installation.projectPath) {
|
|
120
|
+
const projectRoot = resolveProjectPath(installation.projectPath);
|
|
121
|
+
const projectTarget = adapter.getProjectTargetPath(installation);
|
|
122
|
+
results.push(...(await applyScope(sourcePath, type, installation.project, projectTarget, projectRoot)));
|
|
123
|
+
}
|
|
97
124
|
}
|
|
98
125
|
}
|
|
99
126
|
return { results };
|
package/dist/config.js
CHANGED
|
@@ -2,14 +2,19 @@ import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { getConfigPath, getIdeAgentsHome } from "./paths.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
5
|
+
import { defaultAdapterFromIdes, getDefaultIdes, migrateIdesConfig, } from "./ides.js";
|
|
6
|
+
function buildDefaultConfig() {
|
|
7
|
+
const ides = getDefaultIdes();
|
|
8
|
+
return {
|
|
9
|
+
version: 1,
|
|
10
|
+
adapter: defaultAdapterFromIdes(ides),
|
|
11
|
+
ides,
|
|
12
|
+
server: { port: 3921 },
|
|
13
|
+
repos: [],
|
|
14
|
+
installations: [],
|
|
15
|
+
recentProjects: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
13
18
|
const LEGACY_HOME = path.join(homedir(), ".agentdesk");
|
|
14
19
|
async function fileExists(filePath) {
|
|
15
20
|
try {
|
|
@@ -38,6 +43,26 @@ function migrateInstallation(raw) {
|
|
|
38
43
|
projectPath: item.projectPath ?? null,
|
|
39
44
|
};
|
|
40
45
|
}
|
|
46
|
+
function isLegacySampleRepo(repo) {
|
|
47
|
+
if (repo.id === "sample") {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const normalized = repo.url.toLowerCase();
|
|
51
|
+
return (normalized.includes("fixtures/sample-repo") ||
|
|
52
|
+
normalized.includes("fixtures%2fsample-repo"));
|
|
53
|
+
}
|
|
54
|
+
function stripLegacySampleRepo(config) {
|
|
55
|
+
const repos = config.repos.filter((r) => !isLegacySampleRepo(r));
|
|
56
|
+
if (repos.length === config.repos.length) {
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
const removedIds = new Set(config.repos.filter(isLegacySampleRepo).map((r) => r.id));
|
|
60
|
+
return {
|
|
61
|
+
...config,
|
|
62
|
+
repos,
|
|
63
|
+
installations: config.installations.filter((i) => !removedIds.has(i.repoId)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
41
66
|
async function migrateLegacyHome() {
|
|
42
67
|
const home = getIdeAgentsHome();
|
|
43
68
|
if (home === LEGACY_HOME) {
|
|
@@ -57,20 +82,30 @@ export async function ensureIdeAgentsHome() {
|
|
|
57
82
|
export async function readConfig() {
|
|
58
83
|
await ensureIdeAgentsHome();
|
|
59
84
|
const configPath = getConfigPath();
|
|
85
|
+
const defaults = buildDefaultConfig();
|
|
60
86
|
if (!(await fileExists(configPath))) {
|
|
61
|
-
await writeConfig(
|
|
62
|
-
return structuredClone(
|
|
87
|
+
await writeConfig(defaults);
|
|
88
|
+
return structuredClone(defaults);
|
|
63
89
|
}
|
|
64
90
|
const raw = await readFile(configPath, "utf8");
|
|
65
91
|
const parsed = JSON.parse(raw);
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
const ides = migrateIdesConfig(parsed);
|
|
93
|
+
let config = {
|
|
94
|
+
...defaults,
|
|
68
95
|
...parsed,
|
|
69
|
-
|
|
96
|
+
adapter: parsed.adapter ?? defaultAdapterFromIdes(ides),
|
|
97
|
+
ides,
|
|
98
|
+
server: { ...defaults.server, ...parsed.server },
|
|
70
99
|
repos: parsed.repos ?? [],
|
|
71
100
|
installations: (parsed.installations ?? []).map(migrateInstallation),
|
|
72
101
|
recentProjects: parsed.recentProjects ?? [],
|
|
73
102
|
};
|
|
103
|
+
const withoutSample = stripLegacySampleRepo(config);
|
|
104
|
+
if (withoutSample.repos.length !== config.repos.length) {
|
|
105
|
+
await writeConfig(withoutSample);
|
|
106
|
+
config = withoutSample;
|
|
107
|
+
}
|
|
108
|
+
return config;
|
|
74
109
|
}
|
|
75
110
|
export async function writeConfig(config) {
|
|
76
111
|
await ensureIdeAgentsHome();
|
package/dist/git.js
CHANGED
|
@@ -20,10 +20,26 @@ async function runGit(cwd, args) {
|
|
|
20
20
|
});
|
|
21
21
|
return { stdout: result.stdout.trim(), stderr: result.stderr.trim() };
|
|
22
22
|
}
|
|
23
|
+
async function syncExistingRepo(target, ref) {
|
|
24
|
+
try {
|
|
25
|
+
await runGit(target, ["fetch", "--all", "--prune"]);
|
|
26
|
+
try {
|
|
27
|
+
await runGit(target, ["checkout", ref]);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// ref may be a tag or detached state — pull current branch below
|
|
31
|
+
}
|
|
32
|
+
await runGit(target, ["pull", "--ff-only"]);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Best effort: re-attaching a previously removed repo still succeeds
|
|
36
|
+
}
|
|
37
|
+
}
|
|
23
38
|
export async function cloneRepo(url, slug, ref) {
|
|
24
39
|
const target = getRepoPath(slug);
|
|
25
40
|
if (await isGitRepo(target)) {
|
|
26
|
-
|
|
41
|
+
await syncExistingRepo(target, ref);
|
|
42
|
+
return target;
|
|
27
43
|
}
|
|
28
44
|
await execFileAsync("git", ["clone", "--branch", ref, url, target], {
|
|
29
45
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const IDE_AGENTS_GITIGNORE_HEADER = "# ide-agents (managed \u2014 do not edit manually)";
|
|
2
|
+
export declare function toGitignorePath(projectRoot: string, targetPath: string): string | null;
|
|
3
|
+
export declare function isGitRepository(projectRoot: string): Promise<boolean>;
|
|
4
|
+
export declare function addManagedGitignoreEntry(projectRoot: string, entry: string): Promise<void>;
|
|
5
|
+
export declare function removeManagedGitignoreEntry(projectRoot: string, entry: string): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const IDE_AGENTS_GITIGNORE_HEADER = "# ide-agents (managed — do not edit manually)";
|
|
4
|
+
export function toGitignorePath(projectRoot, targetPath) {
|
|
5
|
+
const relative = path.relative(path.resolve(projectRoot), path.resolve(targetPath));
|
|
6
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return relative.split(path.sep).join("/");
|
|
10
|
+
}
|
|
11
|
+
export async function isGitRepository(projectRoot) {
|
|
12
|
+
try {
|
|
13
|
+
await access(path.join(projectRoot, ".git"));
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function parseGitignore(content) {
|
|
21
|
+
const lines = content.split("\n");
|
|
22
|
+
const headerIndex = lines.findIndex((line) => line.trim() === IDE_AGENTS_GITIGNORE_HEADER);
|
|
23
|
+
if (headerIndex === -1) {
|
|
24
|
+
return { before: content.replace(/\n?$/, ""), entries: [], after: "" };
|
|
25
|
+
}
|
|
26
|
+
const before = lines.slice(0, headerIndex).join("\n").replace(/\n?$/, "");
|
|
27
|
+
const entries = [];
|
|
28
|
+
let index = headerIndex + 1;
|
|
29
|
+
while (index < lines.length) {
|
|
30
|
+
const line = lines[index];
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (trimmed === "") {
|
|
33
|
+
index += 1;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.startsWith("#")) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
entries.push(line);
|
|
40
|
+
index += 1;
|
|
41
|
+
}
|
|
42
|
+
const after = lines.slice(index).join("\n").replace(/^\n?/, "");
|
|
43
|
+
return { before, entries, after };
|
|
44
|
+
}
|
|
45
|
+
function formatGitignore(parsed) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (parsed.before.length > 0) {
|
|
48
|
+
parts.push(parsed.before);
|
|
49
|
+
}
|
|
50
|
+
if (parsed.entries.length > 0) {
|
|
51
|
+
parts.push(IDE_AGENTS_GITIGNORE_HEADER);
|
|
52
|
+
parts.push(...parsed.entries);
|
|
53
|
+
parts.push("");
|
|
54
|
+
}
|
|
55
|
+
if (parsed.after.length > 0) {
|
|
56
|
+
parts.push(parsed.after.replace(/\n?$/, ""));
|
|
57
|
+
}
|
|
58
|
+
if (parts.length === 0) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return `${parts.join("\n")}\n`;
|
|
62
|
+
}
|
|
63
|
+
async function readGitignore(gitignorePath) {
|
|
64
|
+
try {
|
|
65
|
+
return await readFile(gitignorePath, "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err.code === "ENOENT") {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function writeGitignoreIfChanged(gitignorePath, nextContent) {
|
|
75
|
+
const current = await readGitignore(gitignorePath);
|
|
76
|
+
if (current === nextContent) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await writeFile(gitignorePath, nextContent, "utf8");
|
|
80
|
+
}
|
|
81
|
+
export async function addManagedGitignoreEntry(projectRoot, entry) {
|
|
82
|
+
if (!(await isGitRepository(projectRoot))) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
86
|
+
const parsed = parseGitignore(await readGitignore(gitignorePath));
|
|
87
|
+
if (parsed.entries.includes(entry)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
parsed.entries.push(entry);
|
|
91
|
+
parsed.entries.sort((a, b) => a.localeCompare(b));
|
|
92
|
+
await writeGitignoreIfChanged(gitignorePath, formatGitignore(parsed));
|
|
93
|
+
}
|
|
94
|
+
export async function removeManagedGitignoreEntry(projectRoot, entry) {
|
|
95
|
+
if (!(await isGitRepository(projectRoot))) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
99
|
+
const parsed = parseGitignore(await readGitignore(gitignorePath));
|
|
100
|
+
if (!parsed.entries.includes(entry)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
parsed.entries = parsed.entries.filter((line) => line !== entry);
|
|
104
|
+
await writeGitignoreIfChanged(gitignorePath, formatGitignore(parsed));
|
|
105
|
+
}
|
package/dist/ides.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdapterId, IdeAgentsConfig, IdeId, IdesConfig } from "./types.js";
|
|
2
|
+
export declare function getDefaultIdes(): IdesConfig;
|
|
3
|
+
export declare function defaultAdapterFromIdes(ides: IdesConfig): AdapterId;
|
|
4
|
+
export declare function migrateIdesConfig(parsed: Partial<IdeAgentsConfig>): IdesConfig;
|
|
5
|
+
export declare function getEnabledIdeIds(config: IdeAgentsConfig): IdeId[];
|
package/dist/ides.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function homeDirExists(dirPath) {
|
|
5
|
+
try {
|
|
6
|
+
return existsSync(dirPath) && statSync(dirPath).isDirectory();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getDefaultIdes() {
|
|
13
|
+
const home = homedir();
|
|
14
|
+
const cursorPath = path.join(home, ".cursor");
|
|
15
|
+
const claudePath = path.join(home, ".claude");
|
|
16
|
+
const codexPath = path.join(home, ".codex");
|
|
17
|
+
return {
|
|
18
|
+
cursor: { enabled: homeDirExists(cursorPath), configPath: cursorPath },
|
|
19
|
+
claude: { enabled: homeDirExists(claudePath), configPath: claudePath },
|
|
20
|
+
codex: { enabled: homeDirExists(codexPath), configPath: codexPath },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function defaultAdapterFromIdes(ides) {
|
|
24
|
+
if (ides.cursor.enabled)
|
|
25
|
+
return "cursor";
|
|
26
|
+
if (ides.claude.enabled)
|
|
27
|
+
return "claude";
|
|
28
|
+
if (ides.codex.enabled)
|
|
29
|
+
return "codex";
|
|
30
|
+
return "cursor";
|
|
31
|
+
}
|
|
32
|
+
export function migrateIdesConfig(parsed) {
|
|
33
|
+
const defaults = getDefaultIdes();
|
|
34
|
+
if (!parsed.ides) {
|
|
35
|
+
return defaults;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
cursor: { ...defaults.cursor, ...parsed.ides.cursor },
|
|
39
|
+
claude: { ...defaults.claude, ...parsed.ides.claude },
|
|
40
|
+
codex: { ...defaults.codex, ...parsed.ides.codex },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function getEnabledIdeIds(config) {
|
|
44
|
+
const ids = [];
|
|
45
|
+
if (config.ides.cursor.enabled)
|
|
46
|
+
ids.push("cursor");
|
|
47
|
+
if (config.ides.claude.enabled)
|
|
48
|
+
ids.push("claude");
|
|
49
|
+
if (config.ides.codex.enabled)
|
|
50
|
+
ids.push("codex");
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export declare function getRepoPath(slug: string): string;
|
|
|
6
6
|
export declare function slugFromUrl(url: string): string;
|
|
7
7
|
export declare function getWebDistDir(): string;
|
|
8
8
|
export declare function resolveProjectPath(projectPath: string): string;
|
|
9
|
+
export declare function expandUserPath(inputPath: string): string;
|
package/dist/paths.js
CHANGED
|
@@ -55,3 +55,13 @@ export function getWebDistDir() {
|
|
|
55
55
|
export function resolveProjectPath(projectPath) {
|
|
56
56
|
return path.resolve(projectPath);
|
|
57
57
|
}
|
|
58
|
+
export function expandUserPath(inputPath) {
|
|
59
|
+
const trimmed = inputPath.trim();
|
|
60
|
+
if (trimmed === "~") {
|
|
61
|
+
return homedir();
|
|
62
|
+
}
|
|
63
|
+
if (trimmed.startsWith("~/")) {
|
|
64
|
+
return path.join(homedir(), trimmed.slice(2));
|
|
65
|
+
}
|
|
66
|
+
return path.resolve(trimmed);
|
|
67
|
+
}
|
package/dist/scan.js
CHANGED
|
@@ -15,7 +15,7 @@ function parseAllowedScope(data) {
|
|
|
15
15
|
if (scope === "global" || scope === "project" || scope === "any") {
|
|
16
16
|
return scope;
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return "any";
|
|
19
19
|
}
|
|
20
20
|
function parseAgentDescription(content, fallbackName) {
|
|
21
21
|
const parsed = matter(content);
|
|
@@ -37,45 +37,74 @@ function parseAgentDescription(content, fallbackName) {
|
|
|
37
37
|
}
|
|
38
38
|
return fallbackName;
|
|
39
39
|
}
|
|
40
|
-
async function
|
|
41
|
-
const
|
|
42
|
-
|
|
40
|
+
async function parseSkillDirectory(skillDir, dirName) {
|
|
41
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
42
|
+
const hasSkillMd = await pathExists(skillMdPath);
|
|
43
|
+
let name = dirName;
|
|
44
|
+
let description = "";
|
|
45
|
+
let allowedScope = "any";
|
|
46
|
+
if (hasSkillMd) {
|
|
47
|
+
const raw = await readFile(skillMdPath, "utf8");
|
|
48
|
+
const parsed = matter(raw);
|
|
49
|
+
if (typeof parsed.data.name === "string" && parsed.data.name.trim()) {
|
|
50
|
+
name = parsed.data.name.trim();
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed.data.description === "string") {
|
|
53
|
+
description = parsed.data.description.trim();
|
|
54
|
+
}
|
|
55
|
+
allowedScope = parseAllowedScope(parsed.data);
|
|
56
|
+
}
|
|
57
|
+
return { id: dirName, name, description, hasSkillMd, allowedScope };
|
|
58
|
+
}
|
|
59
|
+
function toSkillArtifact(sourcePath, parsed) {
|
|
60
|
+
return {
|
|
61
|
+
id: parsed.id,
|
|
62
|
+
kind: "skill",
|
|
63
|
+
sourcePath,
|
|
64
|
+
name: parsed.name,
|
|
65
|
+
description: parsed.description,
|
|
66
|
+
hasSkillMd: parsed.hasSkillMd,
|
|
67
|
+
allowedScope: parsed.allowedScope,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function scanSkillsInDirectory(parentDir, sourcePathPrefix) {
|
|
71
|
+
if (!(await pathExists(parentDir))) {
|
|
43
72
|
return [];
|
|
44
73
|
}
|
|
45
|
-
const entries = await readdir(
|
|
74
|
+
const entries = await readdir(parentDir, { withFileTypes: true });
|
|
46
75
|
const artifacts = [];
|
|
47
76
|
for (const entry of entries) {
|
|
48
77
|
if (!entry.isDirectory())
|
|
49
78
|
continue;
|
|
50
|
-
|
|
79
|
+
if (entry.name.startsWith("."))
|
|
80
|
+
continue;
|
|
81
|
+
const skillDir = path.join(parentDir, entry.name);
|
|
51
82
|
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let description = "";
|
|
55
|
-
let allowedScope = null;
|
|
56
|
-
if (hasSkillMd) {
|
|
57
|
-
const raw = await readFile(skillMdPath, "utf8");
|
|
58
|
-
const parsed = matter(raw);
|
|
59
|
-
if (typeof parsed.data.name === "string" && parsed.data.name.trim()) {
|
|
60
|
-
name = parsed.data.name.trim();
|
|
61
|
-
}
|
|
62
|
-
if (typeof parsed.data.description === "string") {
|
|
63
|
-
description = parsed.data.description.trim();
|
|
64
|
-
}
|
|
65
|
-
allowedScope = parseAllowedScope(parsed.data);
|
|
83
|
+
if (!(await pathExists(skillMdPath))) {
|
|
84
|
+
continue;
|
|
66
85
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
description,
|
|
73
|
-
hasSkillMd,
|
|
74
|
-
allowedScope,
|
|
75
|
-
});
|
|
86
|
+
const parsed = await parseSkillDirectory(skillDir, entry.name);
|
|
87
|
+
const relativeSource = sourcePathPrefix
|
|
88
|
+
? path.join(sourcePathPrefix, entry.name)
|
|
89
|
+
: entry.name;
|
|
90
|
+
artifacts.push(toSkillArtifact(relativeSource, parsed));
|
|
76
91
|
}
|
|
77
92
|
return artifacts.sort((a, b) => a.id.localeCompare(b.id));
|
|
78
93
|
}
|
|
94
|
+
async function scanSkillsNested(repoPath) {
|
|
95
|
+
return scanSkillsInDirectory(path.join(repoPath, "skills"), "skills");
|
|
96
|
+
}
|
|
97
|
+
/** Repos like bluriesophos/cursorskills: `<repo>/<skill-name>/SKILL.md` at root. */
|
|
98
|
+
async function scanSkillsFlat(repoPath) {
|
|
99
|
+
return scanSkillsInDirectory(repoPath, "");
|
|
100
|
+
}
|
|
101
|
+
async function scanSkills(repoPath) {
|
|
102
|
+
const nested = await scanSkillsNested(repoPath);
|
|
103
|
+
if (nested.length > 0) {
|
|
104
|
+
return nested;
|
|
105
|
+
}
|
|
106
|
+
return scanSkillsFlat(repoPath);
|
|
107
|
+
}
|
|
79
108
|
async function scanAgents(repoPath) {
|
|
80
109
|
const agentsDir = path.join(repoPath, "agents");
|
|
81
110
|
if (!(await pathExists(agentsDir))) {
|