ide-agents 0.1.0 → 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 CHANGED
@@ -1,15 +1,18 @@
1
1
  # ide-agents
2
2
 
3
- Local admin for **IDE agents and skills** (Cursor and similar) from any git repository.
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
 
7
+ [![GitHub](https://img.shields.io/badge/GitHub-sergeychernov%2Fide--agents-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sergeychernov/ide-agents)
8
+ [![npm](https://img.shields.io/npm/v/ide-agents?style=for-the-badge&logo=npm&logoColor=white&label=npm)](https://www.npmjs.com/package/ide-agents)
9
+
7
10
  ## What it does
8
11
 
9
12
  - Clones git repositories into `~/.ide-agents/repos/`
10
13
  - 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
14
+ - Creates symlinks in each enabled tool’s config directory and per-project folders
15
+ - Provides a browser UI: Settings, Repositories, Skills, Agents
13
16
 
14
17
  **Not affiliated with Cursor.**
15
18
 
@@ -28,8 +31,8 @@ npm i -g ide-agents
28
31
  Or from source:
29
32
 
30
33
  ```bash
31
- git clone https://github.com/sergeychernov/agentdesk.git
32
- cd agentdesk
34
+ git clone https://github.com/sergeychernov/ide-agents.git
35
+ cd ide-agents
33
36
  npm install
34
37
  npm run build
35
38
  npm i -g .
@@ -56,10 +59,16 @@ ide-agents --port 3922 # custom port
56
59
  ide-agents --no-open # do not open browser
57
60
  ```
58
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
+
59
68
  ## Add a repository
60
69
 
61
- 1. Open **Settings** in the UI
62
- 2. Enter a git URL and branch (default `main`)
70
+ 1. Open **Repositories** in the UI
71
+ 2. Pick a suggested catalog or enter a git URL and branch (default `main`)
63
72
  3. Click **Add / Clone**
64
73
 
65
74
  Your repo should contain:
@@ -86,10 +95,13 @@ Private repos: configure SSH or `gh` auth yourself — ide-agents does not store
86
95
  2. Select a repository
87
96
  3. Click **Global** (🌐) or **Project** (📁) on a card — symlinks apply immediately
88
97
 
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` |
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`.
93
105
 
94
106
  Click the active icon again to remove the symlink (only if target is already a symlink).
95
107
 
@@ -121,11 +133,13 @@ Then add `file://$(pwd)` in the UI.
121
133
 
122
134
  ```
123
135
  ~/.ide-agents/
124
- ├── config.json
136
+ ├── config.json # repos, installations, ides (per-tool enable + paths)
125
137
  └── repos/
126
138
  └── <slug>/ # git clone
127
139
  ```
128
140
 
141
+ Docs: [Settings & IDEs](https://ide-agents.vercel.app/docs/settings) on the project site.
142
+
129
143
  ## License
130
144
 
131
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 { getAdapter } from "./adapters/cursor.js";
4
- import { getRepoPath } from "./paths.js";
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 applyScope(installation, sourcePath, type, enabled, targetPath) {
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
- results.push(await createSymlink(targetPath, sourcePath, type));
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 adapter = getAdapter(config.adapter);
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 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)));
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
- const DEFAULT_CONFIG = {
6
- version: 1,
7
- adapter: "cursor",
8
- server: { port: 3921 },
9
- repos: [],
10
- installations: [],
11
- recentProjects: [],
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(DEFAULT_CONFIG);
62
- return structuredClone(DEFAULT_CONFIG);
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
- return {
67
- ...DEFAULT_CONFIG,
92
+ const ides = migrateIdesConfig(parsed);
93
+ let config = {
94
+ ...defaults,
68
95
  ...parsed,
69
- server: { ...DEFAULT_CONFIG.server, ...parsed.server },
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
- throw new Error(`Repository already cloned at ${target}`);
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
+ }