git0 0.2.12 → 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 +5 -5
- 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 -380
- 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,183 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type { CategorizedRelease, ReleaseAsset, PlatformInfo } from './types.js';
|
|
6
|
+
|
|
7
|
+
/** Emoji and display label for each supported platform key. @internal */
|
|
8
|
+
const PLATFORM_META: Record<string, { emoji: string; label: string }> = {
|
|
9
|
+
windows: { emoji: '🪟', label: 'Windows' },
|
|
10
|
+
macos: { emoji: '🍎', label: 'macOS' },
|
|
11
|
+
linux: { emoji: '🐧', label: 'Linux' },
|
|
12
|
+
universal:{ emoji: '🌐', label: 'Universal' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Fixed display order for platform sections in the menu. @internal */
|
|
16
|
+
const PLATFORM_ORDER = ['windows', 'macos', 'linux', 'universal'] as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds the colored header line shown above each platform's asset group.
|
|
20
|
+
*
|
|
21
|
+
* The header is highlighted in green when it matches the user's current OS or
|
|
22
|
+
* is the `universal` bucket, and dimmed gray otherwise.
|
|
23
|
+
*
|
|
24
|
+
* @param platform - Platform key, e.g. `'linux'`.
|
|
25
|
+
* @param tagName - Release tag, e.g. `'v1.2.3'`.
|
|
26
|
+
* @param isCurrentPlatform - Whether this platform matches the running OS.
|
|
27
|
+
* @returns A formatted `inquirer` separator-style choice object.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
function buildPlatformHeader(
|
|
32
|
+
platform: string,
|
|
33
|
+
tagName: string,
|
|
34
|
+
isCurrentPlatform: boolean
|
|
35
|
+
): { name: string; disabled: true } {
|
|
36
|
+
const { emoji, label } = PLATFORM_META[platform];
|
|
37
|
+
const platformLabel = isCurrentPlatform
|
|
38
|
+
? chalk.green(`${emoji} ${label} (Your Platform)`)
|
|
39
|
+
: chalk.gray(`${emoji} ${label}`);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: `${chalk.bold(tagName)} - ${platformLabel}`,
|
|
43
|
+
disabled: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Builds a selectable `inquirer` choice for a single release asset.
|
|
49
|
+
*
|
|
50
|
+
* Shows the asset filename, detected CPU architecture (when known), and size
|
|
51
|
+
* in MB. The row is white when it belongs to the current platform and gray
|
|
52
|
+
* when it belongs to a different one.
|
|
53
|
+
*
|
|
54
|
+
* @param asset - The release asset to represent.
|
|
55
|
+
* @param isCurrentPlatform - Whether the asset's platform matches the running OS.
|
|
56
|
+
* @param release - Parent release (included in the choice value).
|
|
57
|
+
* @returns An `inquirer` choice object with a `value` of `{ release, asset }`.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
function buildAssetChoice(
|
|
62
|
+
asset: ReleaseAsset,
|
|
63
|
+
isCurrentPlatform: boolean,
|
|
64
|
+
release: CategorizedRelease
|
|
65
|
+
): { name: string; value: { release: CategorizedRelease; asset: ReleaseAsset } } {
|
|
66
|
+
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
67
|
+
const archTag = asset.detectedArch !== 'unknown' && asset.detectedArch !== 'universal'
|
|
68
|
+
? chalk.cyan(`[${asset.detectedArch}]`)
|
|
69
|
+
: '';
|
|
70
|
+
const color = isCurrentPlatform ? chalk.white : chalk.gray;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
name: ` ${color(`${asset.name} ${archTag} (${sizeMB} MB)`)}`,
|
|
74
|
+
value: { release, asset },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Appends a visual separator to `choices` unless the last entry is already a
|
|
80
|
+
* separator. Prevents double-separators between adjacent platform groups.
|
|
81
|
+
*
|
|
82
|
+
* @param choices - Mutable choice list to append into.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
function maybeAddSeparator(choices: any[]): void {
|
|
87
|
+
const last = choices[choices.length - 1];
|
|
88
|
+
if (choices.length > 0 && !last?.name?.includes('────')) {
|
|
89
|
+
choices.push({ name: chalk.gray('────────────────────────────────'), disabled: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Builds the complete flat choice list for `inquirer` from a list of releases.
|
|
95
|
+
*
|
|
96
|
+
* Iterates platforms in {@link PLATFORM_ORDER}, then assets within each
|
|
97
|
+
* platform, inserting section headers and separators as it goes. Only the
|
|
98
|
+
* first `limit` releases are shown to keep the list manageable.
|
|
99
|
+
*
|
|
100
|
+
* @param releases - Categorized release objects to render.
|
|
101
|
+
* @param currentPlatform - The user's platform, used to highlight matching rows.
|
|
102
|
+
* @param limit - Maximum number of releases to display (default 2).
|
|
103
|
+
* @returns Flat array of `inquirer` choice objects (headers, separators, assets).
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const choices = buildReleaseChoices(repo.allReleases, getCurrentPlatform());
|
|
107
|
+
* // → [{ name: 'v1.0 - 🐧 Linux (Your Platform)', disabled: true }, { name: ' app-linux-x64 (12.34 MB)', value: … }, …]
|
|
108
|
+
*/
|
|
109
|
+
export function buildReleaseChoices(
|
|
110
|
+
releases: CategorizedRelease[],
|
|
111
|
+
currentPlatform: PlatformInfo,
|
|
112
|
+
limit = 2
|
|
113
|
+
): any[] {
|
|
114
|
+
const choices: any[] = [];
|
|
115
|
+
|
|
116
|
+
for (const release of releases.slice(0, limit)) {
|
|
117
|
+
for (const platform of PLATFORM_ORDER) {
|
|
118
|
+
const assets = release.platformAssets[platform];
|
|
119
|
+
if (!assets?.length) continue;
|
|
120
|
+
|
|
121
|
+
const isCurrentPlatform = platform === currentPlatform.os || platform === 'universal';
|
|
122
|
+
|
|
123
|
+
maybeAddSeparator(choices);
|
|
124
|
+
choices.push(buildPlatformHeader(platform, release.tag_name, isCurrentPlatform));
|
|
125
|
+
|
|
126
|
+
for (const asset of assets) {
|
|
127
|
+
choices.push(buildAssetChoice(asset, isCurrentPlatform, release));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return choices;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Presents an interactive terminal menu of release packages grouped by
|
|
137
|
+
* platform and prompts the user to pick one to download.
|
|
138
|
+
*
|
|
139
|
+
* The menu shows up to 2 releases (configurable via {@link buildReleaseChoices})
|
|
140
|
+
* with each release's assets grouped under platform headings. The heading for
|
|
141
|
+
* the user's current OS is highlighted in green. After selection, the asset is
|
|
142
|
+
* downloaded to the current working directory.
|
|
143
|
+
*
|
|
144
|
+
* Returns early with a warning when no downloadable assets exist.
|
|
145
|
+
*
|
|
146
|
+
* @param selectedRepo - Repo object that must include `allReleases`.
|
|
147
|
+
* @param downloadPackage - Async function that performs the actual file download.
|
|
148
|
+
* @param currentPlatform - Platform info used to highlight the matching section.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* await showPackageMenu(
|
|
152
|
+
* selectedRepo,
|
|
153
|
+
* github.downloadPackage.bind(github),
|
|
154
|
+
* github.getCurrentPlatform()
|
|
155
|
+
* );
|
|
156
|
+
*/
|
|
157
|
+
export async function showPackageMenu(
|
|
158
|
+
selectedRepo: { allReleases: CategorizedRelease[] },
|
|
159
|
+
downloadPackage: (url: string, dest: string) => Promise<string>,
|
|
160
|
+
currentPlatform: PlatformInfo
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const choices = buildReleaseChoices(selectedRepo.allReleases, currentPlatform);
|
|
163
|
+
const selectableChoices = choices.filter(c => !c.disabled);
|
|
164
|
+
|
|
165
|
+
if (selectableChoices.length === 0) {
|
|
166
|
+
console.log(chalk.yellow('No packages found for download.'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { selectedPackage } = await inquirer.prompt({
|
|
171
|
+
type: 'list',
|
|
172
|
+
name: 'selectedPackage',
|
|
173
|
+
message: 'Select a package to download:',
|
|
174
|
+
choices,
|
|
175
|
+
pageSize: 15,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const downloadDir = path.resolve(process.cwd());
|
|
179
|
+
fs.mkdirSync(downloadDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const downloadPath = path.join(downloadDir, selectedPackage.asset.name);
|
|
182
|
+
await downloadPackage(selectedPackage.asset.browser_download_url, downloadPath);
|
|
183
|
+
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import type { PlatformInfo } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps Node.js `os.platform()` strings to canonical OS names used throughout
|
|
6
|
+
* the release-categorisation logic.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const PLATFORM_MAP: Record<string, string> = {
|
|
11
|
+
win32: 'windows',
|
|
12
|
+
darwin: 'macos',
|
|
13
|
+
linux: 'linux',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps Node.js `os.arch()` strings to the canonical architecture names that
|
|
18
|
+
* are matched against release-asset filenames.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const ARCH_MAP: Record<string, string> = {
|
|
23
|
+
x64: 'x86_64',
|
|
24
|
+
arm64: 'arm64',
|
|
25
|
+
arm: 'arm',
|
|
26
|
+
ia32: 'i386',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns normalized OS and CPU architecture information for the current machine.
|
|
31
|
+
*
|
|
32
|
+
* Node.js uses platform-specific strings (`darwin`, `win32`, `x64`) that differ
|
|
33
|
+
* from the naming conventions used in GitHub release asset filenames. This function
|
|
34
|
+
* maps them to the canonical names used by the rest of the codebase.
|
|
35
|
+
*
|
|
36
|
+
* @returns A {@link PlatformInfo} object with both canonical and raw values.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const p = getCurrentPlatform();
|
|
40
|
+
* // Apple Silicon Mac → { os: 'macos', arch: 'arm64', platform: 'darwin', architecture: 'arm64' }
|
|
41
|
+
* // x86-64 Linux → { os: 'linux', arch: 'x86_64', platform: 'linux', architecture: 'x64' }
|
|
42
|
+
*/
|
|
43
|
+
export function getCurrentPlatform(): PlatformInfo {
|
|
44
|
+
const platform = os.platform();
|
|
45
|
+
const arch = os.arch();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
os: PLATFORM_MAP[platform] || platform,
|
|
49
|
+
arch: ARCH_MAP[arch] || arch,
|
|
50
|
+
platform,
|
|
51
|
+
architecture: arch,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/releases.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { PlatformInfo, ReleaseAsset, PlatformAssets, CategorizedRelease } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filename substrings used to identify which operating system a release asset
|
|
5
|
+
* targets. Matching is case-insensitive and applied to the full asset filename.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const PLATFORM_KEYWORDS: Record<keyof Omit<PlatformAssets, 'universal'>, string[]> = {
|
|
10
|
+
windows: ['win', 'windows', 'win32', 'win64', '.exe', '.msi'],
|
|
11
|
+
macos: ['mac', 'macos', 'darwin', 'osx', '.dmg', '.pkg'],
|
|
12
|
+
linux: ['linux', 'ubuntu', 'debian', '.deb', '.rpm', '.tar.gz', '.AppImage'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Filename substrings used to identify the CPU architecture of a release asset.
|
|
17
|
+
* Matching is case-insensitive and runs after platform detection.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const ARCH_KEYWORDS: Record<string, string[]> = {
|
|
22
|
+
arm64: ['aarch64', 'arm64'], // must come before x86_64 — 'aarch64' contains '64'
|
|
23
|
+
arm: ['armv7', 'armv6'], // must come before bare 'arm' to avoid partial matches
|
|
24
|
+
x86_64: ['x86_64', 'amd64', 'x64'],
|
|
25
|
+
i386: ['i386', 'x86_32'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detects the CPU architecture of a release asset from its filename.
|
|
30
|
+
*
|
|
31
|
+
* Iterates over {@link ARCH_KEYWORDS} in insertion order and returns the first
|
|
32
|
+
* match. Returns `'unknown'` when no keyword matches.
|
|
33
|
+
*
|
|
34
|
+
* @param name - Lowercased asset filename.
|
|
35
|
+
* @returns Canonical arch string, e.g. `'arm64'`, or `'unknown'`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* detectArch('myapp-v1.0-linux-aarch64.tar.gz'); // → 'arm64'
|
|
39
|
+
* detectArch('myapp-v1.0-linux.tar.gz'); // → 'unknown'
|
|
40
|
+
*/
|
|
41
|
+
function detectArch(name: string): string {
|
|
42
|
+
for (const [arch, keys] of Object.entries(ARCH_KEYWORDS)) {
|
|
43
|
+
if (keys.some(k => name.includes(k.toLowerCase()))) return arch;
|
|
44
|
+
}
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Determines whether an asset filename matches any known platform keyword.
|
|
50
|
+
*
|
|
51
|
+
* @param name - Lowercased asset filename.
|
|
52
|
+
* @returns `true` if at least one platform keyword is found.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
function hasKnownPlatformKeyword(name: string): boolean {
|
|
57
|
+
return Object.values(PLATFORM_KEYWORDS)
|
|
58
|
+
.flat()
|
|
59
|
+
.some(kw => name.includes(kw.toLowerCase()));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Classifies a single release asset into a platform bucket and annotates it
|
|
64
|
+
* with the detected architecture.
|
|
65
|
+
*
|
|
66
|
+
* Assets that match no known platform keyword are placed into `universal` if
|
|
67
|
+
* their filename contains `'universal'` or `'all'`, or if it contains none of
|
|
68
|
+
* the three major platform words (`win`, `mac`, `linux`).
|
|
69
|
+
*
|
|
70
|
+
* @param asset - Raw GitHub release asset object.
|
|
71
|
+
* @param platformAssets - Mutable bucket object to push the asset into.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
function classifyAsset(asset: ReleaseAsset, platformAssets: PlatformAssets): void {
|
|
76
|
+
const name = asset.name.toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Explicit universal label wins regardless of any other keyword matches
|
|
79
|
+
// (e.g. 'app-universal.tar.gz' should not be mis-filed under linux).
|
|
80
|
+
if (name.includes('universal')) {
|
|
81
|
+
platformAssets.universal.push({ ...asset, detectedArch: 'universal', platform: 'universal' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let categorized = false;
|
|
86
|
+
|
|
87
|
+
for (const [platform, keywords] of Object.entries(PLATFORM_KEYWORDS)) {
|
|
88
|
+
if (keywords.some(kw => name.includes(kw.toLowerCase()))) {
|
|
89
|
+
(platformAssets as unknown as Record<string, ReleaseAsset[]>)[platform].push({
|
|
90
|
+
...asset,
|
|
91
|
+
detectedArch: detectArch(name),
|
|
92
|
+
platform,
|
|
93
|
+
});
|
|
94
|
+
categorized = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!categorized) {
|
|
99
|
+
platformAssets.universal.push({ ...asset, detectedArch: 'universal', platform: 'universal' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Groups every asset in each release into per-platform buckets.
|
|
105
|
+
*
|
|
106
|
+
* Releases that have no assets in any bucket are dropped from the result so
|
|
107
|
+
* the caller never sees empty entries.
|
|
108
|
+
*
|
|
109
|
+
* @param releases - Raw release array from the GitHub API.
|
|
110
|
+
* @returns Releases annotated with a `platformAssets` map. Only releases that
|
|
111
|
+
* have at least one asset in at least one platform bucket are included.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const categorized = categorizeReleasesByPlatform(rawReleases);
|
|
115
|
+
* categorized[0].platformAssets.linux; // → ReleaseAsset[]
|
|
116
|
+
*/
|
|
117
|
+
export function categorizeReleasesByPlatform(releases: any[]): CategorizedRelease[] {
|
|
118
|
+
const result: CategorizedRelease[] = [];
|
|
119
|
+
|
|
120
|
+
for (const release of Object.values(releases)) {
|
|
121
|
+
const platformAssets: PlatformAssets = { windows: [], macos: [], linux: [], universal: [] };
|
|
122
|
+
|
|
123
|
+
for (const asset of release?.assets ?? []) {
|
|
124
|
+
classifyAsset(asset, platformAssets);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Object.values(platformAssets).some(a => a.length > 0)) {
|
|
128
|
+
result.push({ ...release, platformAssets });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Filters a raw release list down to releases that have at least one asset
|
|
137
|
+
* compatible with the given platform.
|
|
138
|
+
*
|
|
139
|
+
* "Compatible" means the release has assets in the platform's own bucket
|
|
140
|
+
* **or** in the `universal` bucket.
|
|
141
|
+
*
|
|
142
|
+
* @param releases - Raw release array from the GitHub API.
|
|
143
|
+
* @param currentPlatform - Platform info from {@link getCurrentPlatform}.
|
|
144
|
+
* @returns Subset of categorized releases that can run on `currentPlatform`.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const compatible = filterReleasesByPlatform(raw, getCurrentPlatform());
|
|
148
|
+
* // Only releases with linux or universal assets on a Linux machine
|
|
149
|
+
*/
|
|
150
|
+
export function filterReleasesByPlatform(
|
|
151
|
+
releases: any[],
|
|
152
|
+
currentPlatform: PlatformInfo
|
|
153
|
+
): CategorizedRelease[] {
|
|
154
|
+
return categorizeReleasesByPlatform(releases).filter(
|
|
155
|
+
r =>
|
|
156
|
+
(r.platformAssets as unknown as Record<string, ReleaseAsset[]>)[currentPlatform.os]?.length > 0 ||
|
|
157
|
+
r.platformAssets.universal?.length > 0
|
|
158
|
+
);
|
|
159
|
+
}
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the IDE launcher and dependency installer from their dedicated
|
|
3
|
+
* modules. Kept for backwards compatibility with any code that imported from
|
|
4
|
+
* `setup.ts` directly.
|
|
5
|
+
*
|
|
6
|
+
* @module setup
|
|
7
|
+
*/
|
|
8
|
+
export { openInIDE } from './ide.js';
|
|
9
|
+
export { installDependencies } from './install.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized operating system and CPU architecture for the current machine.
|
|
3
|
+
* All string values are lowercased and mapped to a canonical form.
|
|
4
|
+
* @example
|
|
5
|
+
* // On an Apple Silicon Mac:
|
|
6
|
+
* // { os: 'macos', arch: 'arm64', platform: 'darwin', architecture: 'arm64' }
|
|
7
|
+
*/
|
|
8
|
+
export interface PlatformInfo {
|
|
9
|
+
/** Canonical OS name: `'windows'`, `'macos'`, or `'linux'`. */
|
|
10
|
+
os: string;
|
|
11
|
+
/** Canonical arch name: `'x86_64'`, `'arm64'`, `'arm'`, or `'i386'`. */
|
|
12
|
+
arch: string;
|
|
13
|
+
/** Raw `os.platform()` value, e.g. `'darwin'`, `'win32'`, `'linux'`. */
|
|
14
|
+
platform: string;
|
|
15
|
+
/** Raw `os.arch()` value, e.g. `'x64'`, `'arm64'`. */
|
|
16
|
+
architecture: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A single downloadable file attached to a GitHub release,
|
|
21
|
+
* extended with platform/arch fields detected from the filename.
|
|
22
|
+
*/
|
|
23
|
+
export interface ReleaseAsset {
|
|
24
|
+
/** Original filename as uploaded to the GitHub release. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** File size in bytes. */
|
|
27
|
+
size: number;
|
|
28
|
+
/** Direct HTTPS URL to download this asset. */
|
|
29
|
+
browser_download_url: string;
|
|
30
|
+
/** Detected CPU architecture from the filename, or `'unknown'` / `'universal'`. */
|
|
31
|
+
detectedArch: string;
|
|
32
|
+
/** Detected OS platform from the filename, e.g. `'linux'` or `'universal'`. */
|
|
33
|
+
platform: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Release assets bucketed by target operating system.
|
|
39
|
+
* Assets that match no known platform keyword land in `universal`.
|
|
40
|
+
*/
|
|
41
|
+
export interface PlatformAssets {
|
|
42
|
+
windows: ReleaseAsset[];
|
|
43
|
+
macos: ReleaseAsset[];
|
|
44
|
+
linux: ReleaseAsset[];
|
|
45
|
+
/** Assets with no platform keyword, or explicitly labelled `universal` / `all`. */
|
|
46
|
+
universal: ReleaseAsset[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A GitHub release object enriched with per-platform asset buckets.
|
|
51
|
+
*/
|
|
52
|
+
export interface CategorizedRelease {
|
|
53
|
+
/** Git tag name, e.g. `'v1.2.3'`. */
|
|
54
|
+
tag_name: string;
|
|
55
|
+
/** Assets grouped by target platform. */
|
|
56
|
+
platformAssets: PlatformAssets;
|
|
57
|
+
/** Raw asset list from the GitHub API. */
|
|
58
|
+
assets: ReleaseAsset[];
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A GitHub repository search result enriched with release availability flags.
|
|
64
|
+
*/
|
|
65
|
+
export interface SearchResult {
|
|
66
|
+
/** `owner/name` slug, e.g. `'facebook/react'`. */
|
|
67
|
+
full_name: string;
|
|
68
|
+
/** Repository name without owner prefix. */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Short description from the repo's About field. */
|
|
71
|
+
description: string;
|
|
72
|
+
/** Total number of GitHub stars. */
|
|
73
|
+
stargazers_count: number;
|
|
74
|
+
/** Primary programming language detected by GitHub. */
|
|
75
|
+
language: string;
|
|
76
|
+
/** HTTPS URL of the repository. */
|
|
77
|
+
url: string;
|
|
78
|
+
/** Repository owner info. */
|
|
79
|
+
owner: { login: string };
|
|
80
|
+
/** True if the repo has at least one release with any assets. */
|
|
81
|
+
hasReleases: boolean;
|
|
82
|
+
/** True if the repo has at least one release compatible with the current platform. */
|
|
83
|
+
hasCompatibleReleases: boolean;
|
|
84
|
+
/** Releases filtered to the current platform. */
|
|
85
|
+
releases: CategorizedRelease[];
|
|
86
|
+
/** All releases with every platform's assets categorized. */
|
|
87
|
+
allReleases: CategorizedRelease[];
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Candidate IDE entry used when probing for an installed editor. */
|
|
92
|
+
export interface IdeInfo {
|
|
93
|
+
/** Human-readable name shown in log output, e.g. `'VSCode'`. */
|
|
94
|
+
name: string;
|
|
95
|
+
/** Shell command used to invoke the editor, e.g. `'code'`. */
|
|
96
|
+
cmd: string;
|
|
97
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prints the git0 ASCII-art logo in cyan to stdout.
|
|
6
|
+
*
|
|
7
|
+
* Called at the start of every major CLI action so the user always sees the
|
|
8
|
+
* branding regardless of which code path is entered.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* printLogo();
|
|
12
|
+
* // ___
|
|
13
|
+
* // __ _(_)‾|_ / _ \
|
|
14
|
+
* // / _ | | __| | | |
|
|
15
|
+
* // | (_| | | |_| |_| |
|
|
16
|
+
* // \__, |_|\__|\___/
|
|
17
|
+
* // |___/
|
|
18
|
+
*/
|
|
19
|
+
export function printLogo(): void {
|
|
20
|
+
console.log(chalk.cyan(` ___
|
|
21
|
+
__ _(_)‾|_ / _ \\
|
|
22
|
+
/ _ | | __| | | |
|
|
23
|
+
| (_| | | |_| |_| |
|
|
24
|
+
\\__, |_|\\__|\\___/
|
|
25
|
+
|___/`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runs a shell command synchronously, inheriting the parent's stdio so output
|
|
30
|
+
* streams directly to the terminal.
|
|
31
|
+
*
|
|
32
|
+
* Errors are swallowed silently by default because many install commands (e.g.
|
|
33
|
+
* `bun run dev`) exit non-zero when a script is not defined, which should not
|
|
34
|
+
* abort the overall setup flow.
|
|
35
|
+
*
|
|
36
|
+
* @param cmd - Shell command string to execute.
|
|
37
|
+
* @param showError - When `true`, prints a red failure message on non-zero exit.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* exec('npm install');
|
|
41
|
+
* exec('npm run build', true); // prints error if build fails
|
|
42
|
+
*/
|
|
43
|
+
export function exec(cmd: string, showError = false): void {
|
|
44
|
+
try {
|
|
45
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
46
|
+
} catch {
|
|
47
|
+
if (showError) console.error(chalk.red(`❌ Failed: ${cmd}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { getAvailableDirectoryName } from '../src/download.ts';
|
|
6
|
+
|
|
7
|
+
describe('getAvailableDirectoryName', () => {
|
|
8
|
+
let tmp: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'git0-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('returns basePath unchanged when it does not exist', () => {
|
|
19
|
+
const target = path.join(tmp, 'newdir');
|
|
20
|
+
expect(getAvailableDirectoryName(target)).toBe(target);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('appends -2 when basePath already exists', () => {
|
|
24
|
+
const target = path.join(tmp, 'repo');
|
|
25
|
+
fs.mkdirSync(target);
|
|
26
|
+
expect(getAvailableDirectoryName(target)).toBe(`${target}-2`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('increments counter until a free slot is found', () => {
|
|
30
|
+
const target = path.join(tmp, 'repo');
|
|
31
|
+
fs.mkdirSync(target);
|
|
32
|
+
fs.mkdirSync(`${target}-2`);
|
|
33
|
+
fs.mkdirSync(`${target}-3`);
|
|
34
|
+
expect(getAvailableDirectoryName(target)).toBe(`${target}-4`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns basePath when only suffixed variants exist', () => {
|
|
38
|
+
const target = path.join(tmp, 'repo');
|
|
39
|
+
// Create repo-2 but NOT repo — basePath itself is free.
|
|
40
|
+
fs.mkdirSync(`${target}-2`);
|
|
41
|
+
expect(getAvailableDirectoryName(target)).toBe(target);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('works with deeply nested non-existent paths', () => {
|
|
45
|
+
const target = path.join(tmp, 'a', 'b', 'c');
|
|
46
|
+
expect(getAvailableDirectoryName(target)).toBe(target);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import GithubAPI from '../src/github-api.ts';
|
|
3
|
+
|
|
4
|
+
describe('GithubAPI.parseURL', () => {
|
|
5
|
+
const api = new GithubAPI();
|
|
6
|
+
|
|
7
|
+
test('parses full https github.com URL', () => {
|
|
8
|
+
const result = api.parseURL('https://github.com/facebook/react');
|
|
9
|
+
expect(result).not.toBe(false);
|
|
10
|
+
if (!result) return;
|
|
11
|
+
expect(result.owner).toBe('facebook');
|
|
12
|
+
expect(result.name).toBe('react');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('parses owner/repo shorthand', () => {
|
|
16
|
+
const result = api.parseURL('vitejs/vite');
|
|
17
|
+
expect(result).not.toBe(false);
|
|
18
|
+
if (!result) return;
|
|
19
|
+
expect(result.owner).toBe('vitejs');
|
|
20
|
+
expect(result.name).toBe('vite');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('parses SSH git@ URL', () => {
|
|
24
|
+
const result = api.parseURL('git@github.com:torvalds/linux.git');
|
|
25
|
+
expect(result).not.toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('parses git:// protocol URL', () => {
|
|
29
|
+
const result = api.parseURL('git://github.com/user/repo.git');
|
|
30
|
+
expect(result).not.toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('returns false for a plain search query', () => {
|
|
34
|
+
expect(api.parseURL('react starter template')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns false for a single word', () => {
|
|
38
|
+
expect(api.parseURL('react')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns false for an email-like string', () => {
|
|
42
|
+
expect(api.parseURL('user@example.com')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('owner/repo with dots in repo name is accepted', () => {
|
|
46
|
+
const result = api.parseURL('sass/node-sass.git');
|
|
47
|
+
expect(result).not.toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('owner/repo with hyphens is accepted', () => {
|
|
51
|
+
const result = api.parseURL('my-org/my-repo');
|
|
52
|
+
expect(result).not.toBe(false);
|
|
53
|
+
if (!result) return;
|
|
54
|
+
expect(result.name).toBe('my-repo');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('GithubAPI.getCurrentPlatform', () => {
|
|
59
|
+
const api = new GithubAPI();
|
|
60
|
+
|
|
61
|
+
test('returns an object with os, arch, platform, architecture fields', () => {
|
|
62
|
+
const p = api.getCurrentPlatform();
|
|
63
|
+
expect(p).toHaveProperty('os');
|
|
64
|
+
expect(p).toHaveProperty('arch');
|
|
65
|
+
expect(p).toHaveProperty('platform');
|
|
66
|
+
expect(p).toHaveProperty('architecture');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('os is one of the known canonical values or a passthrough', () => {
|
|
70
|
+
const p = api.getCurrentPlatform();
|
|
71
|
+
expect(typeof p.os).toBe('string');
|
|
72
|
+
expect(p.os.length).toBeGreaterThan(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('platform matches process.platform', () => {
|
|
76
|
+
const p = api.getCurrentPlatform();
|
|
77
|
+
expect(p.platform).toBe(process.platform);
|
|
78
|
+
});
|
|
79
|
+
});
|