@quenty/nevermore-cli 4.43.3 → 4.43.4
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/CHANGELOG.md +6 -0
- package/dist/commands/init-command/init-game-command.js +2 -2
- package/dist/commands/init-command/init-game-command.js.map +1 -1
- package/dist/commands/install-package-command.d.ts +14 -0
- package/dist/commands/install-package-command.d.ts.map +1 -1
- package/dist/commands/install-package-command.js +65 -12
- package/dist/commands/install-package-command.js.map +1 -1
- package/dist/utils/package-manager-utils.d.ts +29 -0
- package/dist/utils/package-manager-utils.d.ts.map +1 -0
- package/dist/utils/package-manager-utils.js +89 -0
- package/dist/utils/package-manager-utils.js.map +1 -0
- package/dist/utils/package-manager-utils.test.d.ts +2 -0
- package/dist/utils/package-manager-utils.test.d.ts.map +1 -0
- package/dist/utils/package-manager-utils.test.js +96 -0
- package/dist/utils/package-manager-utils.test.js.map +1 -0
- package/package.json +2 -2
- package/src/commands/init-command/init-game-command.ts +2 -2
- package/src/commands/install-package-command.ts +107 -14
- package/src/utils/package-manager-utils.test.ts +147 -0
- package/src/utils/package-manager-utils.ts +115 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,9 +6,17 @@ import { Argv, CommandModule } from 'yargs';
|
|
|
6
6
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
7
7
|
import { NevermoreGlobalArgs } from '../args/global-args.js';
|
|
8
8
|
import { runCommandAsync } from '../utils/nevermore-cli-utils.js';
|
|
9
|
+
import {
|
|
10
|
+
buildAddPackagesCommand,
|
|
11
|
+
detectPackageManagerAsync,
|
|
12
|
+
isPackageManager,
|
|
13
|
+
PACKAGE_MANAGERS,
|
|
14
|
+
PackageManager,
|
|
15
|
+
} from '../utils/package-manager-utils.js';
|
|
9
16
|
|
|
10
17
|
export interface InstallPackageArgs extends NevermoreGlobalArgs {
|
|
11
18
|
packages: string[];
|
|
19
|
+
packageManager?: string;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
/**
|
|
@@ -27,19 +35,65 @@ export class InstallPackageCommand<T>
|
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Lists the published `@quenty/*` package names, without the scope.
|
|
40
|
+
*
|
|
41
|
+
* The registry caps search results at 250 per request, well under the number
|
|
42
|
+
* of published packages, so this pages through until the reported total is
|
|
43
|
+
* covered.
|
|
44
|
+
*/
|
|
30
45
|
private static async _getPackages(): Promise<string[]> {
|
|
46
|
+
const pageSize = 250;
|
|
47
|
+
const names = new Set<string>();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
let from = 0;
|
|
51
|
+
for (;;) {
|
|
52
|
+
const response = await fetch(
|
|
53
|
+
`https://registry.npmjs.org/-/v1/search?text=@quenty/&size=${pageSize}&from=${from}`
|
|
54
|
+
);
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
const objects: any[] = data.objects ?? [];
|
|
57
|
+
|
|
58
|
+
for (const entry of objects) {
|
|
59
|
+
const name: string = entry.package?.name ?? '';
|
|
60
|
+
if (name.startsWith('@quenty/')) {
|
|
61
|
+
names.add(name.slice('@quenty/'.length));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
from += objects.length;
|
|
66
|
+
if (objects.length < pageSize || from >= (data.total ?? 0)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...names].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Asks the registry about one package directly. Returns undefined when the
|
|
79
|
+
* registry can't be reached, so an offline run doesn't look like a typo.
|
|
80
|
+
*/
|
|
81
|
+
private static async _packageExistsAsync(
|
|
82
|
+
scopedName: string
|
|
83
|
+
): Promise<boolean | undefined> {
|
|
31
84
|
try {
|
|
32
85
|
const response = await fetch(
|
|
33
|
-
|
|
86
|
+
`https://registry.npmjs.org/${scopedName.replace('/', '%2f')}`,
|
|
87
|
+
{ method: 'HEAD' }
|
|
34
88
|
);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
|
|
90
|
+
if (response.status === 404) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return response.ok ? true : undefined;
|
|
41
95
|
} catch {
|
|
42
|
-
return
|
|
96
|
+
return undefined;
|
|
43
97
|
}
|
|
44
98
|
}
|
|
45
99
|
|
|
@@ -53,9 +107,34 @@ export class InstallPackageCommand<T>
|
|
|
53
107
|
return packages.filter((name) => !current || name.startsWith(current));
|
|
54
108
|
},
|
|
55
109
|
});
|
|
110
|
+
args.option('package-manager', {
|
|
111
|
+
describe:
|
|
112
|
+
'Package manager to install with. Detected from the project by default.',
|
|
113
|
+
type: 'string',
|
|
114
|
+
choices: PACKAGE_MANAGERS as unknown as string[],
|
|
115
|
+
});
|
|
56
116
|
return args as Argv<InstallPackageArgs>;
|
|
57
117
|
}
|
|
58
118
|
|
|
119
|
+
private static async _resolvePackageManagerAsync(
|
|
120
|
+
args: InstallPackageArgs,
|
|
121
|
+
srcRoot: string
|
|
122
|
+
): Promise<PackageManager> {
|
|
123
|
+
const requested = args.packageManager;
|
|
124
|
+
if (requested) {
|
|
125
|
+
if (!isPackageManager(requested)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Unsupported package manager '${requested}'. Expected one of: ${PACKAGE_MANAGERS.join(
|
|
128
|
+
', '
|
|
129
|
+
)}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return requested;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return detectPackageManagerAsync(srcRoot);
|
|
136
|
+
}
|
|
137
|
+
|
|
59
138
|
public async handler(args: InstallPackageArgs) {
|
|
60
139
|
const srcRoot = process.cwd();
|
|
61
140
|
|
|
@@ -67,22 +146,36 @@ export class InstallPackageCommand<T>
|
|
|
67
146
|
InstallPackageCommand._validatePackageName(packageName)
|
|
68
147
|
);
|
|
69
148
|
|
|
70
|
-
const
|
|
149
|
+
const prefixedPackages = args.packages.map(
|
|
150
|
+
(packageName) => `@quenty/${packageName}`
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const existence = await Promise.all(
|
|
154
|
+
prefixedPackages.map((scopedName) =>
|
|
155
|
+
InstallPackageCommand._packageExistsAsync(scopedName)
|
|
156
|
+
)
|
|
157
|
+
);
|
|
71
158
|
const invalidPackages = args.packages.filter(
|
|
72
|
-
(
|
|
159
|
+
(_, index) => existence[index] === false
|
|
73
160
|
);
|
|
74
161
|
|
|
75
162
|
if (invalidPackages.length > 0) {
|
|
76
163
|
throw new Error(`Invalid packages: ${invalidPackages.join(', ')}`);
|
|
77
164
|
}
|
|
78
165
|
|
|
79
|
-
const
|
|
80
|
-
(
|
|
166
|
+
const packageManager =
|
|
167
|
+
await InstallPackageCommand._resolvePackageManagerAsync(args, srcRoot);
|
|
168
|
+
|
|
169
|
+
OutputHelper.info(
|
|
170
|
+
`Installing packages with ${packageManager}: ${args.packages.join(', ')}`
|
|
81
171
|
);
|
|
82
172
|
|
|
83
|
-
|
|
173
|
+
const { command, args: commandArgs } = buildAddPackagesCommand(
|
|
174
|
+
packageManager,
|
|
175
|
+
prefixedPackages
|
|
176
|
+
);
|
|
84
177
|
|
|
85
|
-
await runCommandAsync(args,
|
|
178
|
+
await runCommandAsync(args, command, commandArgs, {
|
|
86
179
|
cwd: srcRoot,
|
|
87
180
|
});
|
|
88
181
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
buildAddPackagesCommand,
|
|
7
|
+
detectPackageManagerAsync,
|
|
8
|
+
isPackageManager,
|
|
9
|
+
} from './package-manager-utils.js';
|
|
10
|
+
|
|
11
|
+
describe('package-manager-utils', () => {
|
|
12
|
+
let root: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'nevermore-pm-'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function writeFileAsync(relativePath: string, contents: string) {
|
|
23
|
+
const filePath = path.join(root, relativePath);
|
|
24
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
25
|
+
await fs.writeFile(filePath, contents);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('detectPackageManagerAsync', () => {
|
|
29
|
+
it('reads the packageManager field', async () => {
|
|
30
|
+
await writeFileAsync(
|
|
31
|
+
'package.json',
|
|
32
|
+
JSON.stringify({ packageManager: 'pnpm@10.27.0' })
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(await detectPackageManagerAsync(root)).toBe('pnpm');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('prefers the packageManager field over a lockfile', async () => {
|
|
39
|
+
await writeFileAsync(
|
|
40
|
+
'package.json',
|
|
41
|
+
JSON.stringify({ packageManager: 'yarn@4.0.0' })
|
|
42
|
+
);
|
|
43
|
+
await writeFileAsync('package-lock.json', '{}');
|
|
44
|
+
|
|
45
|
+
expect(await detectPackageManagerAsync(root)).toBe('yarn');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to lockfiles', async () => {
|
|
49
|
+
await writeFileAsync('package.json', JSON.stringify({ name: 'game' }));
|
|
50
|
+
await writeFileAsync('pnpm-lock.yaml', '');
|
|
51
|
+
|
|
52
|
+
expect(await detectPackageManagerAsync(root)).toBe('pnpm');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('treats a pnpm workspace file as pnpm', async () => {
|
|
56
|
+
await writeFileAsync('pnpm-workspace.yaml', 'packages:\n');
|
|
57
|
+
|
|
58
|
+
expect(await detectPackageManagerAsync(root)).toBe('pnpm');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('walks up from a subdirectory', async () => {
|
|
62
|
+
await writeFileAsync(
|
|
63
|
+
'package.json',
|
|
64
|
+
JSON.stringify({ packageManager: 'pnpm@10.27.0' })
|
|
65
|
+
);
|
|
66
|
+
await fs.mkdir(path.join(root, 'src', 'shared'), { recursive: true });
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
await detectPackageManagerAsync(path.join(root, 'src', 'shared'))
|
|
70
|
+
).toBe('pnpm');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('lets the nearest project win', async () => {
|
|
74
|
+
await writeFileAsync(
|
|
75
|
+
'package.json',
|
|
76
|
+
JSON.stringify({ packageManager: 'pnpm@10.27.0' })
|
|
77
|
+
);
|
|
78
|
+
await writeFileAsync(
|
|
79
|
+
'nested/package.json',
|
|
80
|
+
JSON.stringify({ packageManager: 'yarn@4.0.0' })
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(await detectPackageManagerAsync(path.join(root, 'nested'))).toBe(
|
|
84
|
+
'yarn'
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ignores an unknown packageManager field', async () => {
|
|
89
|
+
await writeFileAsync(
|
|
90
|
+
'package.json',
|
|
91
|
+
JSON.stringify({ packageManager: 'cnpm@1.0.0' })
|
|
92
|
+
);
|
|
93
|
+
await writeFileAsync('yarn.lock', '');
|
|
94
|
+
|
|
95
|
+
expect(await detectPackageManagerAsync(root)).toBe('yarn');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('survives malformed package.json', async () => {
|
|
99
|
+
await writeFileAsync('package.json', '{ not json');
|
|
100
|
+
await writeFileAsync('pnpm-lock.yaml', '');
|
|
101
|
+
|
|
102
|
+
expect(await detectPackageManagerAsync(root)).toBe('pnpm');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('defaults to pnpm', async () => {
|
|
106
|
+
expect(await detectPackageManagerAsync(root)).toBe('pnpm');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('still honors an npm project', async () => {
|
|
110
|
+
await writeFileAsync('package.json', JSON.stringify({ name: 'game' }));
|
|
111
|
+
await writeFileAsync('package-lock.json', '{}');
|
|
112
|
+
|
|
113
|
+
expect(await detectPackageManagerAsync(root)).toBe('npm');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('buildAddPackagesCommand', () => {
|
|
118
|
+
it('uses install for npm', () => {
|
|
119
|
+
expect(buildAddPackagesCommand('npm', ['@quenty/blend'])).toEqual({
|
|
120
|
+
command: 'npm',
|
|
121
|
+
args: ['install', '@quenty/blend'],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('uses add for everything else', () => {
|
|
126
|
+
expect(buildAddPackagesCommand('pnpm', ['@quenty/blend'])).toEqual({
|
|
127
|
+
command: 'pnpm',
|
|
128
|
+
args: ['add', '@quenty/blend'],
|
|
129
|
+
});
|
|
130
|
+
expect(buildAddPackagesCommand('yarn', ['@quenty/maid'])).toEqual({
|
|
131
|
+
command: 'yarn',
|
|
132
|
+
args: ['add', '@quenty/maid'],
|
|
133
|
+
});
|
|
134
|
+
expect(buildAddPackagesCommand('bun', ['@quenty/maid'])).toEqual({
|
|
135
|
+
command: 'bun',
|
|
136
|
+
args: ['add', '@quenty/maid'],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('isPackageManager', () => {
|
|
142
|
+
it('accepts known managers and rejects others', () => {
|
|
143
|
+
expect(isPackageManager('pnpm')).toBe(true);
|
|
144
|
+
expect(isPackageManager('deno')).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects which package manager owns a project so commands install with the
|
|
3
|
+
* same tool the project was set up with.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
export const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'] as const;
|
|
10
|
+
|
|
11
|
+
export type PackageManager = typeof PACKAGE_MANAGERS[number];
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_PACKAGE_MANAGER: PackageManager = 'pnpm';
|
|
14
|
+
|
|
15
|
+
const LOCKFILES: ReadonlyArray<readonly [string, PackageManager]> = [
|
|
16
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
17
|
+
['pnpm-workspace.yaml', 'pnpm'],
|
|
18
|
+
['bun.lock', 'bun'],
|
|
19
|
+
['bun.lockb', 'bun'],
|
|
20
|
+
['yarn.lock', 'yarn'],
|
|
21
|
+
['package-lock.json', 'npm'],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function isPackageManager(value: string): value is PackageManager {
|
|
25
|
+
return (PACKAGE_MANAGERS as readonly string[]).includes(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parsePackageManagerField(value: unknown): PackageManager | undefined {
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const name = value.split('@')[0]?.trim();
|
|
34
|
+
if (name && isPackageManager(name)) {
|
|
35
|
+
return name;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function detectInDirectoryAsync(
|
|
42
|
+
directory: string
|
|
43
|
+
): Promise<PackageManager | undefined> {
|
|
44
|
+
try {
|
|
45
|
+
const contents = await fs.readFile(
|
|
46
|
+
path.join(directory, 'package.json'),
|
|
47
|
+
'utf-8'
|
|
48
|
+
);
|
|
49
|
+
const parsed = JSON.parse(contents) as { packageManager?: unknown };
|
|
50
|
+
const declared = parsePackageManagerField(parsed.packageManager);
|
|
51
|
+
if (declared) {
|
|
52
|
+
return declared;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// No readable package.json here, fall through to lockfiles
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [fileName, packageManager] of LOCKFILES) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(path.join(directory, fileName));
|
|
61
|
+
return packageManager;
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detects the package manager for a project, walking up from the given
|
|
72
|
+
* directory so this works from a subfolder of the project.
|
|
73
|
+
*
|
|
74
|
+
* A `packageManager` field wins over a lockfile in the same directory, and the
|
|
75
|
+
* nearest directory with either wins over anything further up. Falls back to
|
|
76
|
+
* pnpm: Nevermore projects are pnpm projects — the templates pin it and gate it
|
|
77
|
+
* behind `only-allow pnpm` — so pnpm is the right guess for a directory that
|
|
78
|
+
* hasn't declared anything yet.
|
|
79
|
+
*/
|
|
80
|
+
export async function detectPackageManagerAsync(
|
|
81
|
+
startDirectory: string
|
|
82
|
+
): Promise<PackageManager> {
|
|
83
|
+
let directory = path.resolve(startDirectory);
|
|
84
|
+
|
|
85
|
+
for (;;) {
|
|
86
|
+
const detected = await detectInDirectoryAsync(directory);
|
|
87
|
+
if (detected) {
|
|
88
|
+
return detected;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parent = path.dirname(directory);
|
|
92
|
+
if (parent === directory) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
directory = parent;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return DEFAULT_PACKAGE_MANAGER;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds the command that adds dependencies to a project. Only npm spells this
|
|
103
|
+
* `install`; the rest treat a bare `install` as "install the lockfile" and need
|
|
104
|
+
* `add` to record new dependencies.
|
|
105
|
+
*/
|
|
106
|
+
export function buildAddPackagesCommand(
|
|
107
|
+
packageManager: PackageManager,
|
|
108
|
+
packages: string[]
|
|
109
|
+
): { command: string; args: string[] } {
|
|
110
|
+
if (packageManager === 'npm') {
|
|
111
|
+
return { command: 'npm', args: ['install', ...packages] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { command: packageManager, args: ['add', ...packages] };
|
|
115
|
+
}
|