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.
Files changed (71) hide show
  1. package/dist/cli.js +27780 -0
  2. package/dist/cli.js.map +331 -0
  3. package/docs/404.html +2 -2
  4. package/docs/Footer/index.html +2 -2
  5. package/docs/assets/js/1df93b7f.1385eba0.js +2 -0
  6. package/docs/assets/js/22dd74f7.e13ac7a4.js +1 -0
  7. package/docs/assets/js/28ab763d.192aa3bb.js +1 -0
  8. package/docs/assets/js/299d276b.c9496717.js +1 -0
  9. package/docs/assets/js/{68cef36b.c312447e.js → 68cef36b.e5b1975b.js} +1 -1
  10. package/docs/assets/js/a9cef9d5.a7253e41.js +1 -0
  11. package/docs/assets/js/c3a618e1.1c56fb03.js +590 -0
  12. package/docs/assets/js/{main.cf858a7d.js → main.1f646b09.js} +2 -2
  13. package/docs/assets/js/runtime~main.f69f8f44.js +1 -0
  14. package/docs/functions/fm/index.html +26 -0
  15. package/docs/functions/git0/index.html +6 -6
  16. package/docs/functions/github-api/index.html +20 -20
  17. package/docs/functions/index.html +889 -71
  18. package/docs/functions/modules/index.html +4 -3
  19. package/docs/index.html +2 -2
  20. package/docs/lunr-index-1749760982052.json +1 -0
  21. package/docs/lunr-index.json +1 -1
  22. package/docs/search-doc-1749760982052.json +1 -0
  23. package/docs/search-doc.json +1 -1
  24. package/docs/sitemap.xml +1 -1
  25. package/docs-config/.docusaurus/client-manifest.json +36 -24
  26. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/p/index-466.json +1 -1
  27. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-fm-md-a9c.json +19 -0
  28. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-git-0-md-299.json +4 -0
  29. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-index-md-c3a.json +1 -1
  30. package/docs-config/.docusaurus/globalData.json +10 -5
  31. package/docs-config/.docusaurus/registry.js +1 -0
  32. package/docs-config/.docusaurus/routes.js +9 -3
  33. package/docs-config/.docusaurus/routesChunkNames.json +7 -3
  34. package/docs-config/src/functions/fm.md +1 -0
  35. package/docs-config/src/functions/git0.md +2 -2
  36. package/docs-config/src/functions/github-api.md +34 -17
  37. package/docs-config/src/functions/index.md +19 -30
  38. package/docs-config/src/functions/modules.md +1 -0
  39. package/docs-config/src/functions/typedoc-sidebar.cjs +5 -0
  40. package/docs-config/src/pages/index.tsx +2 -3
  41. package/package.json +20 -10
  42. package/readme.md +19 -21
  43. package/src/cli.ts +150 -0
  44. package/src/download.ts +240 -0
  45. package/src/fm.js +0 -0
  46. package/src/github-api.ts +237 -0
  47. package/src/ide.ts +141 -0
  48. package/src/install.ts +147 -0
  49. package/src/package-menu.ts +183 -0
  50. package/src/platform.ts +53 -0
  51. package/src/releases.ts +159 -0
  52. package/src/setup.ts +9 -0
  53. package/src/types.ts +97 -0
  54. package/src/utils.ts +49 -0
  55. package/test/download.test.ts +48 -0
  56. package/test/github-api.test.ts +79 -0
  57. package/test/package-menu.test.ts +134 -0
  58. package/test/platform.test.ts +64 -0
  59. package/test/releases.test.ts +169 -0
  60. package/docs/assets/js/1df93b7f.d8c05d2c.js +0 -2
  61. package/docs/assets/js/22dd74f7.237398b4.js +0 -1
  62. package/docs/assets/js/28ab763d.5714aa16.js +0 -1
  63. package/docs/assets/js/299d276b.1a1baa1c.js +0 -1
  64. package/docs/assets/js/c3a618e1.965a31da.js +0 -1
  65. package/docs/assets/js/runtime~main.7520dc36.js +0 -1
  66. package/docs/lunr-index-1749613752315.json +0 -1
  67. package/docs/search-doc-1749613752315.json +0 -1
  68. package/src/git0.js +0 -379
  69. package/src/github-api.js +0 -472
  70. /package/docs/assets/js/{1df93b7f.d8c05d2c.js.LICENSE.txt → 1df93b7f.1385eba0.js.LICENSE.txt} +0 -0
  71. /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
+ }
@@ -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
+ }
@@ -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
+ });