@rindrics/initrepo 0.0.1 → 0.1.5
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/.github/codeql/codeql-config.yml +7 -0
- package/.github/dependabot.yml +11 -0
- package/.github/release.yml +4 -0
- package/.github/workflows/ci.yml +67 -0
- package/.github/workflows/codeql.yml +46 -0
- package/.github/workflows/publish.yml +35 -0
- package/.github/workflows/tagpr.yml +21 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-push +2 -0
- package/.tagpr +7 -0
- package/.tool-versions +1 -0
- package/CHANGELOG.md +28 -0
- package/README.md +40 -28
- package/biome.json +38 -0
- package/bun.lock +334 -0
- package/commitlint.config.js +3 -0
- package/dist/cli.js +11215 -0
- package/docs/adr/0001-simple-module-structure-over-ddd.md +111 -0
- package/package.json +37 -7
- package/src/cli.test.ts +20 -0
- package/src/cli.ts +27 -0
- package/src/commands/init.test.ts +170 -0
- package/src/commands/init.ts +172 -0
- package/src/commands/prepare-release.test.ts +183 -0
- package/src/commands/prepare-release.ts +354 -0
- package/src/config.ts +13 -0
- package/src/generators/project.test.ts +363 -0
- package/src/generators/project.ts +300 -0
- package/src/templates/common/dependabot.yml.ejs +12 -0
- package/src/templates/common/release.yml.ejs +4 -0
- package/src/templates/common/workflows/tagpr.yml.ejs +31 -0
- package/src/templates/typescript/.tagpr.ejs +5 -0
- package/src/templates/typescript/codeql/codeql-config.yml.ejs +7 -0
- package/src/templates/typescript/package.json.ejs +29 -0
- package/src/templates/typescript/src/index.ts.ejs +1 -0
- package/src/templates/typescript/tsconfig.json.ejs +17 -0
- package/src/templates/typescript/workflows/ci.yml.ejs +58 -0
- package/src/templates/typescript/workflows/codeql.yml.ejs +46 -0
- package/src/types.ts +13 -0
- package/src/utils/github-repo.test.ts +34 -0
- package/src/utils/github-repo.ts +141 -0
- package/src/utils/github.ts +47 -0
- package/src/utils/npm.test.ts +99 -0
- package/src/utils/npm.ts +59 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: tagpr
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
tagpr:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
17
|
+
<% if (isDevcode) { -%>
|
|
18
|
+
# TODO: After replace-devcode, add token: ${{ secrets.PAT_FOR_TAGPR }}
|
|
19
|
+
<% } else { -%>
|
|
20
|
+
with:
|
|
21
|
+
token: ${{ secrets.PAT_FOR_TAGPR }}
|
|
22
|
+
<% } -%>
|
|
23
|
+
|
|
24
|
+
- uses: Songmu/tagpr@<%= actionVersions['Songmu/tagpr'] %>
|
|
25
|
+
env:
|
|
26
|
+
<% if (isDevcode) { -%>
|
|
27
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
# TODO: After replace-devcode, use PAT_FOR_TAGPR instead
|
|
29
|
+
<% } else { -%>
|
|
30
|
+
GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}
|
|
31
|
+
<% } -%>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= name %>",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
<% if (isDevcode) { -%>
|
|
5
|
+
"private": true,
|
|
6
|
+
<% } -%>
|
|
7
|
+
"description": "",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun run src/index.ts",
|
|
11
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
12
|
+
"lint": "biome lint src",
|
|
13
|
+
"format": "biome format src --write",
|
|
14
|
+
"check": "biome check src",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"prepare": "husky"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "<%= author %>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "^<%= versions['@biomejs/biome'] %>",
|
|
23
|
+
"@commitlint/cli": "^<%= versions['@commitlint/cli'] %>",
|
|
24
|
+
"@commitlint/config-conventional": "^<%= versions['@commitlint/config-conventional'] %>",
|
|
25
|
+
"bun-types": "^<%= versions['bun-types'] %>",
|
|
26
|
+
"husky": "^<%= versions['husky'] %>",
|
|
27
|
+
"typescript": "^<%= versions['typescript'] %>"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log('Hello from <%= name %>!');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"types": ["bun-types"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- 'src/**'
|
|
9
|
+
- 'package.json'
|
|
10
|
+
- 'tsconfig.json'
|
|
11
|
+
- 'biome.json'
|
|
12
|
+
- 'bun.lock'
|
|
13
|
+
- '.github/workflows/ci.yml'
|
|
14
|
+
push:
|
|
15
|
+
branches:
|
|
16
|
+
- main
|
|
17
|
+
paths:
|
|
18
|
+
- 'src/**'
|
|
19
|
+
- 'package.json'
|
|
20
|
+
- 'tsconfig.json'
|
|
21
|
+
- 'biome.json'
|
|
22
|
+
- 'bun.lock'
|
|
23
|
+
- '.github/workflows/ci.yml'
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
check:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
33
|
+
|
|
34
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
35
|
+
with:
|
|
36
|
+
bun-version: latest
|
|
37
|
+
|
|
38
|
+
- run: bun install --frozen-lockfile
|
|
39
|
+
|
|
40
|
+
- name: Biome check
|
|
41
|
+
run: bun run check
|
|
42
|
+
|
|
43
|
+
- name: TypeScript check
|
|
44
|
+
run: bun run build
|
|
45
|
+
|
|
46
|
+
test:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
50
|
+
|
|
51
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
52
|
+
with:
|
|
53
|
+
bun-version: latest
|
|
54
|
+
|
|
55
|
+
- run: bun install --frozen-lockfile
|
|
56
|
+
|
|
57
|
+
- name: Test
|
|
58
|
+
run: bun test
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'src/**'
|
|
8
|
+
- 'package.json'
|
|
9
|
+
- 'tsconfig.json'
|
|
10
|
+
- '.github/workflows/codeql.yml'
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: [main]
|
|
13
|
+
paths:
|
|
14
|
+
- 'src/**'
|
|
15
|
+
- 'package.json'
|
|
16
|
+
- 'tsconfig.json'
|
|
17
|
+
- '.github/workflows/codeql.yml'
|
|
18
|
+
schedule:
|
|
19
|
+
# Run weekly on Sunday at 00:00 UTC
|
|
20
|
+
- cron: '0 0 * * 0'
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
analyze:
|
|
25
|
+
name: Analyze
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
permissions:
|
|
29
|
+
actions: read
|
|
30
|
+
contents: read
|
|
31
|
+
security-events: write
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout repository
|
|
35
|
+
uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
36
|
+
|
|
37
|
+
- name: Initialize CodeQL
|
|
38
|
+
uses: github/codeql-action/init@<%= actionVersions['github/codeql-action'] %>
|
|
39
|
+
with:
|
|
40
|
+
languages: javascript-typescript
|
|
41
|
+
config-file: ./.github/codeql/codeql-config.yml
|
|
42
|
+
|
|
43
|
+
- name: Perform CodeQL Analysis
|
|
44
|
+
uses: github/codeql-action/analyze@<%= actionVersions['github/codeql-action'] %>
|
|
45
|
+
with:
|
|
46
|
+
category: '/language:javascript-typescript'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Language = 'typescript';
|
|
2
|
+
|
|
3
|
+
export interface InitOptions {
|
|
4
|
+
/** The name used for npm publish (e.g., @scope/package-name) */
|
|
5
|
+
projectName: string;
|
|
6
|
+
lang: Language;
|
|
7
|
+
/** If true, projectName is a development code that will be replaced later */
|
|
8
|
+
isDevcode: boolean;
|
|
9
|
+
/** Optional output directory path. Defaults to projectName if not specified. */
|
|
10
|
+
targetDir?: string;
|
|
11
|
+
/** Package author. If not specified, detected from language-specific tools (e.g., npm whoami for TypeScript). */
|
|
12
|
+
author?: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { hasGitHubToken } from './github-repo';
|
|
3
|
+
|
|
4
|
+
describe('github-repo utils', () => {
|
|
5
|
+
describe('hasGitHubToken', () => {
|
|
6
|
+
const originalToken = process.env.GITHUB_TOKEN;
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (originalToken !== undefined) {
|
|
10
|
+
process.env.GITHUB_TOKEN = originalToken;
|
|
11
|
+
} else {
|
|
12
|
+
delete process.env.GITHUB_TOKEN;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should return true when GITHUB_TOKEN is set', () => {
|
|
17
|
+
process.env.GITHUB_TOKEN = 'test-token';
|
|
18
|
+
expect(hasGitHubToken()).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should return false when GITHUB_TOKEN is not set', () => {
|
|
22
|
+
delete process.env.GITHUB_TOKEN;
|
|
23
|
+
expect(hasGitHubToken()).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should return false when GITHUB_TOKEN is empty', () => {
|
|
27
|
+
process.env.GITHUB_TOKEN = '';
|
|
28
|
+
expect(hasGitHubToken()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Note: createGitHubRepo is not tested here because it requires
|
|
33
|
+
// actual GitHub API calls. Integration tests should be done separately.
|
|
34
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Octokit } from 'octokit';
|
|
2
|
+
|
|
3
|
+
export interface CreateRepoOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
isPrivate: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface GitHubRepoResult {
|
|
10
|
+
url: string;
|
|
11
|
+
cloneUrl: string;
|
|
12
|
+
alreadyExisted: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Labels required for tagpr workflow
|
|
17
|
+
*/
|
|
18
|
+
const TAGPR_LABELS = [
|
|
19
|
+
{
|
|
20
|
+
name: 'tagpr:minor',
|
|
21
|
+
color: '0e8a16',
|
|
22
|
+
description: 'have tagpr bump minor version',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'tagpr:major',
|
|
26
|
+
color: 'd93f0b',
|
|
27
|
+
description: 'have tagpr bump major version',
|
|
28
|
+
},
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export class RepoAlreadyExistsError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
public readonly repoName: string,
|
|
34
|
+
public readonly repoUrl: string,
|
|
35
|
+
public readonly cloneUrl: string,
|
|
36
|
+
) {
|
|
37
|
+
super(`Repository "${repoName}" already exists`);
|
|
38
|
+
this.name = 'RepoAlreadyExistsError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a GitHub repository and sets up tagpr labels
|
|
44
|
+
*/
|
|
45
|
+
export async function createGitHubRepo(
|
|
46
|
+
options: CreateRepoOptions,
|
|
47
|
+
): Promise<GitHubRepoResult> {
|
|
48
|
+
const token = process.env.GITHUB_TOKEN;
|
|
49
|
+
|
|
50
|
+
if (!token) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'GITHUB_TOKEN environment variable is required for repository creation',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const octokit = new Octokit({ auth: token });
|
|
57
|
+
|
|
58
|
+
// Get authenticated user
|
|
59
|
+
const { data: user } = await octokit.rest.users.getAuthenticated();
|
|
60
|
+
|
|
61
|
+
let repoUrl: string;
|
|
62
|
+
let cloneUrl: string;
|
|
63
|
+
let alreadyExisted = false;
|
|
64
|
+
|
|
65
|
+
// Create repository
|
|
66
|
+
try {
|
|
67
|
+
const { data: repo } = await octokit.rest.repos.createForAuthenticatedUser({
|
|
68
|
+
name: options.name,
|
|
69
|
+
description: options.description ?? '',
|
|
70
|
+
private: options.isPrivate,
|
|
71
|
+
auto_init: false,
|
|
72
|
+
});
|
|
73
|
+
repoUrl = repo.html_url;
|
|
74
|
+
cloneUrl = repo.clone_url;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const apiError = error as { status?: number; message?: string };
|
|
77
|
+
|
|
78
|
+
// Repository already exists (422 Unprocessable Entity)
|
|
79
|
+
if (apiError.status === 422) {
|
|
80
|
+
// Fetch existing repo info
|
|
81
|
+
try {
|
|
82
|
+
const { data: existingRepo } = await octokit.rest.repos.get({
|
|
83
|
+
owner: user.login,
|
|
84
|
+
repo: options.name,
|
|
85
|
+
});
|
|
86
|
+
repoUrl = existingRepo.html_url;
|
|
87
|
+
cloneUrl = existingRepo.clone_url;
|
|
88
|
+
alreadyExisted = true;
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Repository "${options.name}" already exists but could not fetch its details`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create tagpr labels (skip silently if already exist)
|
|
100
|
+
await createTagprLabels(octokit, user.login, options.name);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
url: repoUrl,
|
|
104
|
+
cloneUrl,
|
|
105
|
+
alreadyExisted,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates tagpr labels for a repository
|
|
111
|
+
*/
|
|
112
|
+
async function createTagprLabels(
|
|
113
|
+
octokit: Octokit,
|
|
114
|
+
owner: string,
|
|
115
|
+
repo: string,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
for (const label of TAGPR_LABELS) {
|
|
118
|
+
try {
|
|
119
|
+
await octokit.rest.issues.createLabel({
|
|
120
|
+
owner,
|
|
121
|
+
repo,
|
|
122
|
+
name: label.name,
|
|
123
|
+
color: label.color,
|
|
124
|
+
description: label.description,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Label already exists (422), ignore
|
|
128
|
+
const apiError = error as { status?: number };
|
|
129
|
+
if (apiError.status !== 422) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Checks if GITHUB_TOKEN is available
|
|
138
|
+
*/
|
|
139
|
+
export function hasGitHubToken(): boolean {
|
|
140
|
+
return !!process.env.GITHUB_TOKEN;
|
|
141
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DEFAULT_ACTION_VERSION, GITHUB_ACTIONS } from '../config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetches the latest major version tag for a GitHub Action
|
|
5
|
+
* @param repo - Repository in format "owner/repo" (e.g., "actions/checkout")
|
|
6
|
+
* @returns Latest major version tag (e.g., "v4")
|
|
7
|
+
*/
|
|
8
|
+
export async function getLatestActionVersion(repo: string): Promise<string> {
|
|
9
|
+
const response = await fetch(
|
|
10
|
+
`https://api.github.com/repos/${repo}/releases/latest`,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to fetch latest release for ${repo}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data = (await response.json()) as { tag_name: string };
|
|
18
|
+
// Extract major version (e.g., "v4.2.0" -> "v4")
|
|
19
|
+
const match = data.tag_name.match(/^v?(\d+)/);
|
|
20
|
+
return match ? `v${match[1]}` : data.tag_name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetches latest major versions for all configured GitHub Actions
|
|
25
|
+
*/
|
|
26
|
+
export async function getLatestActionVersions(
|
|
27
|
+
actions: readonly string[] = Object.keys(GITHUB_ACTIONS),
|
|
28
|
+
): Promise<Record<string, string>> {
|
|
29
|
+
const versions: Record<string, string> = {};
|
|
30
|
+
|
|
31
|
+
await Promise.all(
|
|
32
|
+
actions.map(async (action) => {
|
|
33
|
+
try {
|
|
34
|
+
versions[action] = await getLatestActionVersion(action);
|
|
35
|
+
} catch {
|
|
36
|
+
versions[action] =
|
|
37
|
+
GITHUB_ACTIONS[action as keyof typeof GITHUB_ACTIONS] ??
|
|
38
|
+
DEFAULT_ACTION_VERSION;
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return versions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-export for convenience
|
|
47
|
+
export { GITHUB_ACTIONS };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { getLatestVersion, getLatestVersions, getNpmUsername } from './npm';
|
|
3
|
+
|
|
4
|
+
describe('npm utils', () => {
|
|
5
|
+
describe('getLatestVersion', () => {
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = originalFetch;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('should return version from npm registry', async () => {
|
|
13
|
+
globalThis.fetch = mock(() =>
|
|
14
|
+
Promise.resolve({
|
|
15
|
+
ok: true,
|
|
16
|
+
json: () => Promise.resolve({ version: '1.2.3' }),
|
|
17
|
+
} as Response),
|
|
18
|
+
) as unknown as typeof fetch;
|
|
19
|
+
|
|
20
|
+
const version = await getLatestVersion('some-package');
|
|
21
|
+
expect(version).toBe('1.2.3');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should throw error when fetch fails', async () => {
|
|
25
|
+
globalThis.fetch = mock(() =>
|
|
26
|
+
Promise.resolve({
|
|
27
|
+
ok: false,
|
|
28
|
+
} as Response),
|
|
29
|
+
) as unknown as typeof fetch;
|
|
30
|
+
|
|
31
|
+
expect(getLatestVersion('some-package')).rejects.toThrow(
|
|
32
|
+
'Failed to fetch version',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getLatestVersions', () => {
|
|
38
|
+
const originalFetch = globalThis.fetch;
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
globalThis.fetch = originalFetch;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should return versions for multiple packages', async () => {
|
|
45
|
+
globalThis.fetch = mock((url: string) => {
|
|
46
|
+
const packageName = url.toString().split('/').slice(-2, -1)[0];
|
|
47
|
+
const versions: Record<string, string> = {
|
|
48
|
+
'package-a': '1.0.0',
|
|
49
|
+
'package-b': '2.0.0',
|
|
50
|
+
};
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
ok: true,
|
|
53
|
+
json: () => Promise.resolve({ version: versions[packageName] }),
|
|
54
|
+
} as Response);
|
|
55
|
+
}) as unknown as typeof fetch;
|
|
56
|
+
|
|
57
|
+
const versions = await getLatestVersions(['package-a', 'package-b']);
|
|
58
|
+
expect(versions['package-a']).toBe('1.0.0');
|
|
59
|
+
expect(versions['package-b']).toBe('2.0.0');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should fallback to "latest" when fetch fails', async () => {
|
|
63
|
+
globalThis.fetch = mock(() =>
|
|
64
|
+
Promise.resolve({
|
|
65
|
+
ok: false,
|
|
66
|
+
} as Response),
|
|
67
|
+
) as unknown as typeof fetch;
|
|
68
|
+
|
|
69
|
+
const versions = await getLatestVersions(['failing-package']);
|
|
70
|
+
expect(versions['failing-package']).toBe('latest');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getNpmUsername', () => {
|
|
75
|
+
test('should return username when logged in', async () => {
|
|
76
|
+
const mockExec = mock(() =>
|
|
77
|
+
Promise.resolve({ stdout: 'mocked-user\n', stderr: '' }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const username = await getNpmUsername(mockExec);
|
|
81
|
+
expect(username).toBe('mocked-user');
|
|
82
|
+
expect(mockExec).toHaveBeenCalledWith('npm whoami');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should return null when stdout is empty', async () => {
|
|
86
|
+
const mockExec = mock(() => Promise.resolve({ stdout: '', stderr: '' }));
|
|
87
|
+
|
|
88
|
+
const username = await getNpmUsername(mockExec);
|
|
89
|
+
expect(username).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should return null when npm whoami fails', async () => {
|
|
93
|
+
const mockExec = mock(() => Promise.reject(new Error('Not logged in')));
|
|
94
|
+
|
|
95
|
+
const username = await getNpmUsername(mockExec);
|
|
96
|
+
expect(username).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/utils/npm.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
type ExecFn = (command: string) => Promise<{ stdout: string; stderr: string }>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetches the latest version of a package from npm registry
|
|
10
|
+
*/
|
|
11
|
+
export async function getLatestVersion(packageName: string): Promise<string> {
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Failed to fetch version for ${packageName}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = (await response.json()) as { version: string };
|
|
21
|
+
return data.version;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the current npm username if logged in
|
|
26
|
+
* @param execFn - Optional exec function for testing (defaults to execAsync)
|
|
27
|
+
*/
|
|
28
|
+
export async function getNpmUsername(
|
|
29
|
+
execFn: ExecFn = execAsync,
|
|
30
|
+
): Promise<string | null> {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execFn('npm whoami');
|
|
33
|
+
return stdout.trim() || null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetches latest versions for multiple packages
|
|
41
|
+
*/
|
|
42
|
+
export async function getLatestVersions(
|
|
43
|
+
packageNames: string[],
|
|
44
|
+
): Promise<Record<string, string>> {
|
|
45
|
+
const versions: Record<string, string> = {};
|
|
46
|
+
|
|
47
|
+
await Promise.all(
|
|
48
|
+
packageNames.map(async (name) => {
|
|
49
|
+
try {
|
|
50
|
+
versions[name] = await getLatestVersion(name);
|
|
51
|
+
} catch {
|
|
52
|
+
// Fallback to a default version if fetch fails
|
|
53
|
+
versions[name] = 'latest';
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return versions;
|
|
59
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"types": ["bun-types"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|