git0 0.2.13 → 0.2.14
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/dist/cli.js +27780 -0
- package/dist/cli.js.map +331 -0
- package/docs/404.html +2 -2
- package/docs/Footer/index.html +2 -2
- package/docs/assets/js/1df93b7f.1385eba0.js +2 -0
- package/docs/assets/js/22dd74f7.e13ac7a4.js +1 -0
- package/docs/assets/js/28ab763d.192aa3bb.js +1 -0
- package/docs/assets/js/299d276b.c9496717.js +1 -0
- package/docs/assets/js/{68cef36b.c312447e.js → 68cef36b.e5b1975b.js} +1 -1
- package/docs/assets/js/a9cef9d5.a7253e41.js +1 -0
- package/docs/assets/js/c3a618e1.1c56fb03.js +590 -0
- package/docs/assets/js/{main.cf858a7d.js → main.1f646b09.js} +2 -2
- package/docs/assets/js/runtime~main.f69f8f44.js +1 -0
- package/docs/functions/fm/index.html +26 -0
- package/docs/functions/git0/index.html +6 -6
- package/docs/functions/github-api/index.html +20 -20
- package/docs/functions/index.html +889 -71
- package/docs/functions/modules/index.html +4 -3
- package/docs/index.html +2 -2
- package/docs/lunr-index-1749760982052.json +1 -0
- package/docs/lunr-index.json +1 -1
- package/docs/search-doc-1749760982052.json +1 -0
- package/docs/search-doc.json +1 -1
- package/docs/sitemap.xml +1 -1
- package/docs-config/.docusaurus/client-manifest.json +36 -24
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/p/index-466.json +1 -1
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-fm-md-a9c.json +19 -0
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-git-0-md-299.json +4 -0
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-index-md-c3a.json +1 -1
- package/docs-config/.docusaurus/globalData.json +10 -5
- package/docs-config/.docusaurus/registry.js +1 -0
- package/docs-config/.docusaurus/routes.js +9 -3
- package/docs-config/.docusaurus/routesChunkNames.json +7 -3
- package/docs-config/src/functions/fm.md +1 -0
- package/docs-config/src/functions/git0.md +2 -2
- package/docs-config/src/functions/github-api.md +34 -17
- package/docs-config/src/functions/index.md +19 -30
- package/docs-config/src/functions/modules.md +1 -0
- package/docs-config/src/functions/typedoc-sidebar.cjs +5 -0
- package/docs-config/src/pages/index.tsx +2 -3
- package/package.json +20 -10
- package/readme.md +19 -21
- package/src/cli.ts +150 -0
- package/src/download.ts +240 -0
- package/src/fm.js +0 -0
- package/src/github-api.ts +237 -0
- package/src/ide.ts +141 -0
- package/src/install.ts +147 -0
- package/src/package-menu.ts +183 -0
- package/src/platform.ts +53 -0
- package/src/releases.ts +159 -0
- package/src/setup.ts +9 -0
- package/src/types.ts +97 -0
- package/src/utils.ts +49 -0
- package/test/download.test.ts +48 -0
- package/test/github-api.test.ts +79 -0
- package/test/package-menu.test.ts +134 -0
- package/test/platform.test.ts +64 -0
- package/test/releases.test.ts +169 -0
- package/docs/assets/js/1df93b7f.d8c05d2c.js +0 -2
- package/docs/assets/js/22dd74f7.237398b4.js +0 -1
- package/docs/assets/js/28ab763d.5714aa16.js +0 -1
- package/docs/assets/js/299d276b.1a1baa1c.js +0 -1
- package/docs/assets/js/c3a618e1.965a31da.js +0 -1
- package/docs/assets/js/runtime~main.7520dc36.js +0 -1
- package/docs/lunr-index-1749613752315.json +0 -1
- package/docs/search-doc-1749613752315.json +0 -1
- package/src/git0.js +0 -379
- package/src/github-api.js +0 -472
- /package/docs/assets/js/{1df93b7f.d8c05d2c.js.LICENSE.txt → 1df93b7f.1385eba0.js.LICENSE.txt} +0 -0
- /package/docs/assets/js/{main.cf858a7d.js.LICENSE.txt → main.1f646b09.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { grab } from 'grab-api.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import gitUrlParse from 'git-url-parse';
|
|
4
|
+
import type { PlatformInfo, CategorizedRelease, SearchResult } from './types.js';
|
|
5
|
+
import { getCurrentPlatform } from './platform.js';
|
|
6
|
+
import { categorizeReleasesByPlatform, filterReleasesByPlatform } from './releases.js';
|
|
7
|
+
import { downloadRepo, downloadPackage } from './download.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GitHub API client for searching repositories, downloading source tarballs,
|
|
11
|
+
* and fetching release assets.
|
|
12
|
+
*
|
|
13
|
+
* Internally uses a `grab` instance pre-configured with the GitHub base URL,
|
|
14
|
+
* optional bearer token, and a 403 rate-limit handler.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const github = new GithubAPI({ token: process.env.GITHUB_TOKEN });
|
|
18
|
+
* const repos = await github.searchRepositories('nodejs template');
|
|
19
|
+
* const dir = await github.downloadRepo('facebook/react');
|
|
20
|
+
*/
|
|
21
|
+
class GithubAPI {
|
|
22
|
+
/** Default number of repository search results returned per query. */
|
|
23
|
+
static DEFAULT_RESULTS_PER_PAGE = 10;
|
|
24
|
+
|
|
25
|
+
private callGithub: ReturnType<typeof grab.instance>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new GithubAPI instance and initialises the underlying HTTP client.
|
|
29
|
+
*
|
|
30
|
+
* When no `token` is supplied the constructor falls back to the
|
|
31
|
+
* `GITHUB_TOKEN` environment variable. Unauthenticated requests are
|
|
32
|
+
* limited to 60/hour by GitHub; a token raises this to 5 000/hour.
|
|
33
|
+
*
|
|
34
|
+
* @param options.token - GitHub personal access token.
|
|
35
|
+
* @param options.debug - Pass `true` to enable `grab` request logging.
|
|
36
|
+
* @param options.baseURL - Override the GitHub REST API base URL
|
|
37
|
+
* (useful for GitHub Enterprise).
|
|
38
|
+
*/
|
|
39
|
+
constructor(options: { token?: string; debug?: boolean; baseURL?: string } = {}) {
|
|
40
|
+
const token = options.token || process.env.GITHUB_TOKEN;
|
|
41
|
+
const debug = options.debug ?? false;
|
|
42
|
+
const baseURL = options.baseURL || 'https://api.github.com';
|
|
43
|
+
|
|
44
|
+
this.callGithub = grab.instance({
|
|
45
|
+
debug,
|
|
46
|
+
baseURL,
|
|
47
|
+
timeout: 500,
|
|
48
|
+
headers: token ? { Authorization: `token ${token}` } : {},
|
|
49
|
+
onError: (error: string) => {
|
|
50
|
+
if (error.includes('403')) {
|
|
51
|
+
console.log(chalk.red(
|
|
52
|
+
'Rate limit exceeded. Set the GITHUB_TOKEN env var.\n' +
|
|
53
|
+
'Create a token at https://github.com/settings/personal-access-tokens/new'
|
|
54
|
+
));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Downloads a GitHub repository as a tarball and extracts it locally.
|
|
63
|
+
*
|
|
64
|
+
* Delegates to {@link downloadRepo} in `download.ts`.
|
|
65
|
+
* Falls back from the `master` branch to `main` when the first attempt fails.
|
|
66
|
+
*
|
|
67
|
+
* @param repo - Full GitHub URL or `owner/repo` shorthand.
|
|
68
|
+
* @param targetDir - Optional extraction folder name; defaults to the repo name.
|
|
69
|
+
* @returns Absolute path of the extracted project directory.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const dir = await github.downloadRepo('https://github.com/vitejs/vite');
|
|
73
|
+
* // → '/current/working/dir/vite'
|
|
74
|
+
*/
|
|
75
|
+
async downloadRepo(repo: string, targetDir: string | null = null): Promise<string> {
|
|
76
|
+
return downloadRepo(this.callGithub, repo, targetDir);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Searches GitHub repositories by name and enriches each result with
|
|
81
|
+
* release availability information.
|
|
82
|
+
*
|
|
83
|
+
* Results are sorted by stars descending by default. When `getReleaseInfo`
|
|
84
|
+
* is true (the default), the function fires one extra API request per result
|
|
85
|
+
* to fetch release data — all requests run concurrently via `Promise.all`.
|
|
86
|
+
*
|
|
87
|
+
* @param query - Search string matched against repository names.
|
|
88
|
+
* @param options.perPage - Maximum results (default {@link DEFAULT_RESULTS_PER_PAGE}).
|
|
89
|
+
* @param options.sort - GitHub sort field: `'stars'` | `'forks'` | `'updated'`.
|
|
90
|
+
* @param options.order - Sort direction: `'asc'` | `'desc'`.
|
|
91
|
+
* @param options.getReleaseInfo - When `false`, skips the per-repo release fetch.
|
|
92
|
+
* @returns Array of {@link SearchResult} objects ordered by `sort`/`order`.
|
|
93
|
+
* @throws Re-throws any network or API error after logging it.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const repos = await github.searchRepositories('react starter');
|
|
97
|
+
* repos.forEach(r => console.log(r.full_name, r.hasCompatibleReleases));
|
|
98
|
+
*/
|
|
99
|
+
async searchRepositories(
|
|
100
|
+
query: string,
|
|
101
|
+
options: { perPage?: number; sort?: string; order?: string; getReleaseInfo?: boolean } = {}
|
|
102
|
+
): Promise<SearchResult[]> {
|
|
103
|
+
const {
|
|
104
|
+
perPage = GithubAPI.DEFAULT_RESULTS_PER_PAGE,
|
|
105
|
+
sort = 'stars',
|
|
106
|
+
order = 'desc',
|
|
107
|
+
getReleaseInfo = true,
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await this.callGithub('/search/repositories', {
|
|
112
|
+
q: `${query} in:name`,
|
|
113
|
+
sort,
|
|
114
|
+
order,
|
|
115
|
+
per_page: perPage,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (response.error || !response.items) {
|
|
119
|
+
console.log('No response');
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!getReleaseInfo) return response.items;
|
|
124
|
+
|
|
125
|
+
return Promise.all(
|
|
126
|
+
response.items.map(async (repo: SearchResult) => {
|
|
127
|
+
const releases = await this.callGithub(
|
|
128
|
+
`/repos/${repo.owner.login}/${repo.name}/releases`
|
|
129
|
+
);
|
|
130
|
+
const platform = getCurrentPlatform();
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...repo,
|
|
134
|
+
hasReleases: releases?.length > 0,
|
|
135
|
+
hasCompatibleReleases: filterReleasesByPlatform(releases, platform).length > 0,
|
|
136
|
+
releases: filterReleasesByPlatform(releases, platform),
|
|
137
|
+
allReleases: categorizeReleasesByPlatform(releases),
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
console.error(chalk.red('Search failed:'), error.message);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Downloads a single release asset binary to disk.
|
|
149
|
+
*
|
|
150
|
+
* Delegates to {@link downloadPackage} in `download.ts`.
|
|
151
|
+
*
|
|
152
|
+
* @param packageURL - Direct HTTPS download URL for the asset.
|
|
153
|
+
* @param downloadPath - Absolute local path to write the file to.
|
|
154
|
+
* @returns The `downloadPath` on success.
|
|
155
|
+
* @throws When the download request fails.
|
|
156
|
+
*/
|
|
157
|
+
async downloadPackage(packageURL: string, downloadPath: string): Promise<string> {
|
|
158
|
+
return downloadPackage(this.callGithub, packageURL, downloadPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parses a GitHub URL or `owner/repo` shorthand into a structured URL object.
|
|
163
|
+
*
|
|
164
|
+
* Accepts:
|
|
165
|
+
* - Full HTTPS URLs: `https://github.com/owner/repo`
|
|
166
|
+
* - SSH URLs: `git@github.com:owner/repo`
|
|
167
|
+
* - Git protocol: `git://github.com/owner/repo`
|
|
168
|
+
* - Shorthand: `owner/repo`
|
|
169
|
+
*
|
|
170
|
+
* @param query - The string to parse.
|
|
171
|
+
* @returns A `git-url-parse` result object, or `false` when the input does
|
|
172
|
+
* not look like a GitHub reference.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* github.parseURL('facebook/react');
|
|
176
|
+
* // → { owner: 'facebook', name: 'react', href: 'https://github.com/facebook/react', … }
|
|
177
|
+
*
|
|
178
|
+
* github.parseURL('just a search query'); // → false
|
|
179
|
+
*/
|
|
180
|
+
parseURL(query: string): ReturnType<typeof gitUrlParse> | false {
|
|
181
|
+
if (
|
|
182
|
+
query.includes('github.com') ||
|
|
183
|
+
query.startsWith('git@github.com:') ||
|
|
184
|
+
query.startsWith('https://') ||
|
|
185
|
+
query.startsWith('git://')
|
|
186
|
+
) {
|
|
187
|
+
return gitUrlParse(query);
|
|
188
|
+
}
|
|
189
|
+
if (/^[\w-]+\/[\w.-]+$/.test(query)) {
|
|
190
|
+
return gitUrlParse(`https://github.com/${query}`);
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns normalized OS and CPU architecture for the current machine.
|
|
197
|
+
*
|
|
198
|
+
* Delegates to {@link getCurrentPlatform} from `platform.ts`.
|
|
199
|
+
*
|
|
200
|
+
* @returns {@link PlatformInfo} with canonical OS/arch strings.
|
|
201
|
+
*/
|
|
202
|
+
getCurrentPlatform(): PlatformInfo {
|
|
203
|
+
return getCurrentPlatform();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fetches all releases for a repo and categorizes their assets by platform.
|
|
208
|
+
*
|
|
209
|
+
* @param owner - GitHub username or organisation name.
|
|
210
|
+
* @param repo - Repository name.
|
|
211
|
+
* @returns Array of {@link CategorizedRelease} objects, one per release that
|
|
212
|
+
* has at least one downloadable asset.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* const releases = await github.getReleases('microsoft', 'vscode');
|
|
216
|
+
*/
|
|
217
|
+
async getReleases(owner: string, repo: string): Promise<CategorizedRelease[]> {
|
|
218
|
+
const releases = await this.callGithub(`/repos/${owner}/${repo}/releases`);
|
|
219
|
+
return categorizeReleasesByPlatform(releases);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Fetches releases that have at least one asset compatible with the current
|
|
224
|
+
* operating system.
|
|
225
|
+
*
|
|
226
|
+
* @param owner - GitHub username or organisation name.
|
|
227
|
+
* @param repo - Repository name.
|
|
228
|
+
* @returns Subset of {@link CategorizedRelease} objects compatible with the
|
|
229
|
+
* current platform (including `universal` assets).
|
|
230
|
+
*/
|
|
231
|
+
async getCompatibleReleases(owner: string, repo: string): Promise<CategorizedRelease[]> {
|
|
232
|
+
const releases = await this.callGithub(`/repos/${owner}/${repo}/releases`);
|
|
233
|
+
return filterReleasesByPlatform(releases, getCurrentPlatform());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default GithubAPI;
|
package/src/ide.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync, spawn } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type { IdeInfo } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ordered list of editors to probe. The first one found on `PATH` wins.
|
|
9
|
+
* Add new entries at the top to give them higher priority.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const SUPPORTED_IDES: IdeInfo[] = [
|
|
14
|
+
{ name: 'Antigravity', cmd: 'antigravity' },
|
|
15
|
+
{ name: 'Cursor', cmd: 'cursor' },
|
|
16
|
+
{ name: 'Windsurf', cmd: 'windsurf' },
|
|
17
|
+
{ name: 'VSCode', cmd: 'code' },
|
|
18
|
+
{ name: 'Code Server', cmd: 'code-server' },
|
|
19
|
+
{ name: 'Neovim', cmd: 'nvim' },
|
|
20
|
+
{ name: 'Webstorm', cmd: 'webstorm' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Probes `PATH` for the first IDE in {@link SUPPORTED_IDES} that is installed.
|
|
25
|
+
*
|
|
26
|
+
* Uses `where` on Windows and `command -v` on Unix. The check is intentionally
|
|
27
|
+
* silent (`stdio: 'ignore'`) so no output leaks into the CLI.
|
|
28
|
+
*
|
|
29
|
+
* @returns The first installed {@link IdeInfo}, or `null` when none are found.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const ide = getInstalledIde();
|
|
33
|
+
* if (ide) console.log(`Found: ${ide.name}`);
|
|
34
|
+
*/
|
|
35
|
+
export function getInstalledIde(): IdeInfo | null {
|
|
36
|
+
const probe = process.platform === 'win32'
|
|
37
|
+
? (cmd: string) => `where ${cmd}`
|
|
38
|
+
: (cmd: string) => `command -v ${cmd}`;
|
|
39
|
+
|
|
40
|
+
for (const ide of SUPPORTED_IDES) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(probe(ide.cmd), { stdio: 'ignore' });
|
|
43
|
+
return ide;
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds the argument list for spawning an editor process.
|
|
53
|
+
*
|
|
54
|
+
* `code-server` requires a `--open` flag to actually open the browser;
|
|
55
|
+
* all other editors accept a bare path.
|
|
56
|
+
*
|
|
57
|
+
* @param ide - The IDE to build args for.
|
|
58
|
+
* @param target - Path to open (directory or file).
|
|
59
|
+
* @returns Argument array suitable for `spawn(ide.cmd, args)`.
|
|
60
|
+
*
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
function buildIdeArgs(ide: IdeInfo, target: string): string[] {
|
|
64
|
+
return ide.cmd === 'code-server' ? [target, '--open'] : [target];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Locates the entry-point document to open after the IDE loads the project
|
|
69
|
+
* folder. Checks for README files first (case-insensitive), then falls back
|
|
70
|
+
* to `package.json`.
|
|
71
|
+
*
|
|
72
|
+
* @param dir - Directory to search inside (defaults to `process.cwd()`).
|
|
73
|
+
* @returns Relative path of the first match, or `null` if nothing is found.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
function findEntryDocument(dir = '.'): string | null {
|
|
78
|
+
const candidates = [
|
|
79
|
+
`${dir}/readme.md`,
|
|
80
|
+
`${dir}/Readme.md`,
|
|
81
|
+
`${dir}/README.md`,
|
|
82
|
+
`${dir}/package.json`,
|
|
83
|
+
];
|
|
84
|
+
return candidates.find(p => fs.existsSync(p)) ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Spawns an IDE process in a detached, fire-and-forget manner.
|
|
89
|
+
*
|
|
90
|
+
* The process is detached and unreffed so it outlives the parent CLI process.
|
|
91
|
+
* Shell mode is enabled on Windows to handle PATH resolution correctly.
|
|
92
|
+
*
|
|
93
|
+
* @param ide - IDE to launch.
|
|
94
|
+
* @param args - Arguments to pass (e.g. path to open).
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
function spawnDetached(ide: IdeInfo, args: string[]): void {
|
|
99
|
+
spawn(ide.cmd, args, {
|
|
100
|
+
detached: true,
|
|
101
|
+
stdio: 'ignore',
|
|
102
|
+
shell: process.platform === 'win32',
|
|
103
|
+
}).unref();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Opens a project directory in the first available IDE and, after a short
|
|
108
|
+
* delay, also opens the project's entry document (README or `package.json`).
|
|
109
|
+
*
|
|
110
|
+
* The 3-second delay gives the IDE time to finish loading the workspace before
|
|
111
|
+
* it receives the file-open command — opening too early can cause some editors
|
|
112
|
+
* to ignore the second argument.
|
|
113
|
+
*
|
|
114
|
+
* Does nothing and prints a warning when no supported IDE is installed.
|
|
115
|
+
*
|
|
116
|
+
* @param targetDir - Absolute path to the project directory to open.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* openInIDE('/home/user/projects/my-app');
|
|
120
|
+
* // Logs: 🚀 Opening my-app in Cursor
|
|
121
|
+
*/
|
|
122
|
+
export function openInIDE(targetDir: string): void {
|
|
123
|
+
const ide = getInstalledIde();
|
|
124
|
+
if (!ide) {
|
|
125
|
+
console.log(chalk.yellow('⚠️ No supported IDE found'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
spawnDetached(ide, buildIdeArgs(ide, targetDir));
|
|
131
|
+
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
const doc = findEntryDocument(targetDir);
|
|
134
|
+
if (doc) spawnDetached(ide, buildIdeArgs(ide, doc));
|
|
135
|
+
}, 3000);
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green(`🚀 Opening ${path.basename(targetDir)} in ${ide.name}`));
|
|
138
|
+
} catch {
|
|
139
|
+
// Swallow — IDE launch errors are non-fatal for the download workflow.
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/install.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { exec } from './utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A detector checks whether a specific project type is present in the current
|
|
7
|
+
* working directory by looking for a characteristic file.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
type Detector = () => boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An installer runs the appropriate package manager or build tool for a
|
|
15
|
+
* detected project type.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
type Installer = () => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when the `bun` binary is available on `PATH`.
|
|
23
|
+
*
|
|
24
|
+
* Uses `where` on Windows and `command -v` on Unix. Throws when not found
|
|
25
|
+
* (callers catch this to fall back to npm).
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
function bunAvailable(): boolean {
|
|
30
|
+
try {
|
|
31
|
+
execSync(
|
|
32
|
+
process.platform === 'win32' ? 'where bun' : 'command -v bun',
|
|
33
|
+
{ stdio: 'ignore' }
|
|
34
|
+
);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Installs Node.js dependencies and starts the dev/start script.
|
|
43
|
+
*
|
|
44
|
+
* Prefers `bun` when available; falls back to `npm`. Runs `dev` first and
|
|
45
|
+
* `start` as a fallback in the same shell invocation — both are expected to
|
|
46
|
+
* fail gracefully when the script doesn't exist.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
function installNode(): void {
|
|
51
|
+
if (bunAvailable()) {
|
|
52
|
+
exec('bun install');
|
|
53
|
+
exec('bun run dev; bun run start');
|
|
54
|
+
} else {
|
|
55
|
+
exec('npm install');
|
|
56
|
+
exec('npm run dev; npm run start');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Starts a Docker project using Compose if `docker-compose.yml` is present,
|
|
62
|
+
* otherwise builds a plain Docker image from `Dockerfile`.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
function installDocker(): void {
|
|
67
|
+
if (fs.existsSync('docker-compose.yml')) {
|
|
68
|
+
exec('sudo docker-compose up -d');
|
|
69
|
+
} else if (fs.existsSync('Dockerfile')) {
|
|
70
|
+
exec('sudo docker build -t project .');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a Python virtualenv, activates it, and installs dependencies.
|
|
76
|
+
*
|
|
77
|
+
* Installs from `requirements.txt` and/or `setup.py` depending on which
|
|
78
|
+
* files exist. The `source .venv/bin/activate` call is a no-op on Windows
|
|
79
|
+
* (where the command is not available) but is harmless to include.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
function installPython(): void {
|
|
84
|
+
exec('python -m venv .venv');
|
|
85
|
+
exec('source .venv/bin/activate');
|
|
86
|
+
if (fs.existsSync('requirements.txt')) exec('pip install -r requirements.txt');
|
|
87
|
+
if (fs.existsSync('setup.py')) exec('pip install -e .');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Project-type detectors keyed by ecosystem name.
|
|
92
|
+
* Each function returns `true` when it recognises the current directory.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
const DETECTORS: Record<string, Detector> = {
|
|
97
|
+
nodejs: () => fs.existsSync('package.json'),
|
|
98
|
+
docker: () => fs.existsSync('Dockerfile') || fs.existsSync('docker-compose.yml'),
|
|
99
|
+
python: () => fs.existsSync('requirements.txt') || fs.existsSync('setup.py'),
|
|
100
|
+
rust: () => fs.existsSync('Cargo.toml'),
|
|
101
|
+
go: () => fs.existsSync('go.mod'),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Per-ecosystem install handlers, keyed to match {@link DETECTORS}.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
const INSTALLERS: Record<string, Installer> = {
|
|
110
|
+
nodejs: installNode,
|
|
111
|
+
docker: installDocker,
|
|
112
|
+
python: installPython,
|
|
113
|
+
rust: () => exec('cargo build'),
|
|
114
|
+
go: () => exec('go mod tidy'),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Detects the project type(s) in `targetDir` and runs the appropriate
|
|
119
|
+
* dependency installer(s).
|
|
120
|
+
*
|
|
121
|
+
* A directory can match multiple project types simultaneously (e.g. a Node.js
|
|
122
|
+
* project that also has a Dockerfile). All matched installers are run in order.
|
|
123
|
+
*
|
|
124
|
+
* Supported ecosystems:
|
|
125
|
+
* | Ecosystem | Detection file | Install command |
|
|
126
|
+
* |-----------|------------------------|---------------------------|
|
|
127
|
+
* | Node.js | `package.json` | `bun install` / `npm i` |
|
|
128
|
+
* | Docker | `Dockerfile` / Compose | `docker-compose up -d` |
|
|
129
|
+
* | Python | `requirements.txt` | `pip install -r …` |
|
|
130
|
+
* | Rust | `Cargo.toml` | `cargo build` |
|
|
131
|
+
* | Go | `go.mod` | `go mod tidy` |
|
|
132
|
+
*
|
|
133
|
+
* @param targetDir - Absolute path to the project root to install into.
|
|
134
|
+
* The function changes the process working directory to this path before
|
|
135
|
+
* running any commands.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* await installDependencies('/home/user/projects/my-node-app');
|
|
139
|
+
* // Detects package.json → runs bun install && bun run dev
|
|
140
|
+
*/
|
|
141
|
+
export async function installDependencies(targetDir: string): Promise<void> {
|
|
142
|
+
process.chdir(targetDir);
|
|
143
|
+
|
|
144
|
+
for (const [name, detect] of Object.entries(DETECTORS)) {
|
|
145
|
+
if (detect()) INSTALLERS[name]?.();
|
|
146
|
+
}
|
|
147
|
+
}
|