Skip to content

Commit 2c67d40

Browse files
committed
feat: xpm set-config
1 parent 338d6a0 commit 2c67d40

5 files changed

Lines changed: 108 additions & 7 deletions

File tree

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# xpm - Universal Package Manager Wrapper
22

3-
`xpm` is built for those who work across multiple projects with different package managers. You can use `xpm` instead of `npm`/`yarn`/`pnpm`/`bun`. `xpm` invokes the right package manager for you. Additonally, it installs package dependencies if you forgot to do it.
3+
Working across multiple projects with different package managers?
4+
5+
You can use `xpm` instead of `npm`/`yarn`/`pnpm`/`bun`.
6+
7+
Think of `xpm` as your universal translator for package managers. When you run `xpm`, it figures out which package manager your project uses, ensures your dependencies are up-to-date, and runs the appropriate command. It's like having muscle memory that works everywhere.
48

59
For example, when you run `xpm dev` in a project:
610

7-
- xpm detects the package manager
8-
- auto-installs dependencies if necessary (compares lockfile hash)
11+
- `xpm` detects the package manager
12+
- it auto-installs dependencies if necessary (compares lockfile hash)
913
- runs `npm run dev` / `yarn dev` / `pnpm dev` / `bun dev`
1014

15+
1116
## Install
1217

1318
```bash
@@ -33,6 +38,20 @@ xpm dev # Run a script
3338
xpx prettier # Download and run from the registry
3439
```
3540

41+
## Configuration
42+
43+
Set the default package manager for new projects:
44+
45+
```bash
46+
xpm set-config default-package-manager pnpm
47+
```
48+
49+
The default is used when no lockfile or package.json `packageManager` field is found.
50+
51+
You can also set it via environment variable:
52+
```bash
53+
export XPM_DEFAULT_PM=bun
54+
```
3655

3756
## License
3857

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@assistant-ui/xpm",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "Universal package manager wrapper that automatically detects and uses the right package manager",
55
"main": "dist/index.js",
66
"bin": {

src/config.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
2+
import { homedir } from 'os';
3+
import { join, dirname } from 'path';
4+
import { PackageManager } from './package-manager-config';
5+
6+
const CONFIG_PATH = join(homedir(), '.xpmrc');
7+
8+
export function getDefaultPackageManager(): PackageManager {
9+
// 1. Check environment variable
10+
const envPM = process.env.XPM_DEFAULT_PM;
11+
if (envPM && isValidPackageManager(envPM)) {
12+
return envPM as PackageManager;
13+
}
14+
15+
// 2. Check config file in home directory
16+
if (existsSync(CONFIG_PATH)) {
17+
try {
18+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
19+
if (config.defaultPackageManager && isValidPackageManager(config.defaultPackageManager)) {
20+
return config.defaultPackageManager;
21+
}
22+
} catch {}
23+
}
24+
25+
// 3. Fall back to npm
26+
return 'npm';
27+
}
28+
29+
export function setDefaultPackageManager(pm: string): void {
30+
if (!isValidPackageManager(pm)) {
31+
throw new Error(`Invalid package manager: ${pm}. Must be one of: npm, yarn, pnpm, bun`);
32+
}
33+
34+
let config: any = {};
35+
if (existsSync(CONFIG_PATH)) {
36+
try {
37+
config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
38+
} catch {}
39+
}
40+
41+
config.defaultPackageManager = pm;
42+
43+
// Ensure directory exists
44+
const dir = dirname(CONFIG_PATH);
45+
if (!existsSync(dir)) {
46+
mkdirSync(dir, { recursive: true });
47+
}
48+
49+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
50+
console.log(`Default package manager set to: ${pm}`);
51+
}
52+
53+
function isValidPackageManager(pm: string): boolean {
54+
return ['npm', 'yarn', 'pnpm', 'bun'].includes(pm);
55+
}

src/detector.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import { PackageManager, PACKAGE_MANAGERS, SUPPORTED_PACKAGE_MANAGERS } from './package-manager-config';
4+
import { getDefaultPackageManager } from './config';
45

56
export interface DetectionResult {
67
packageManager: PackageManager;
@@ -47,7 +48,7 @@ export function detectPackageManager(startDir = process.cwd()): DetectionResult
4748
const isWorkspace = lockfileDir !== null && lockfileDir !== packageJsonDir;
4849

4950
// Try to read package.json for corepack config
50-
let detectedPM: PackageManager = 'npm';
51+
let detectedPM: PackageManager | null = null;
5152
try {
5253
const packageJson = JSON.parse(fs.readFileSync(path.join(detectionRoot, 'package.json'), 'utf-8'));
5354
const pmField = packageJson.packageManager ?? packageJson.devEngines?.packageManager?.name;
@@ -60,7 +61,7 @@ export function detectPackageManager(startDir = process.cwd()): DetectionResult
6061
} catch {}
6162

6263
// Check for lockfiles if no corepack config
63-
if (detectedPM === 'npm') {
64+
if (!detectedPM) {
6465
for (const manager of ['bun', 'pnpm', 'yarn', 'npm'] as PackageManager[]) {
6566
if (fs.existsSync(path.join(detectionRoot, PACKAGE_MANAGERS[manager].lockfile))) {
6667
detectedPM = manager;
@@ -69,6 +70,11 @@ export function detectPackageManager(startDir = process.cwd()): DetectionResult
6970
}
7071
}
7172

73+
// Fall back to configured default
74+
if (!detectedPM) {
75+
detectedPM = getDefaultPackageManager();
76+
}
77+
7278
return {
7379
packageManager: detectedPM,
7480
projectRoot: packageJsonDir,

src/xpm.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
22
import { detectPackageManager, shouldRunAtWorkspaceRoot } from './detector';
33
import { synchronizeDependencies } from './dependency-synchronizer';
44
import { mapCommand } from './command-mapper';
5+
import { setDefaultPackageManager } from './config';
56

67
const skipInstallCommands = ['install', 'i', 'add', 'remove', 'uninstall', 'update', 'upgrade'];
78

@@ -53,19 +54,39 @@ Commands:
5354
install/i/add [pkg] Install package(s)
5455
remove/rm/uninstall Remove package
5556
upgrade Update packages
57+
set-config Set configuration
5658
[script] Run package.json script
5759
5860
Flags: --dry-run, --version/-v, --help/-h
5961
62+
Config:
63+
xpm set-config default-package-manager <npm|yarn|pnpm|bun>
64+
6065
Detects: npm/yarn/pnpm/bun`);
6166
}
6267

6368
run(): void {
6469
this.parseFlags();
6570

71+
// Handle set-config command
72+
const [command, ...args] = this.args;
73+
if (command === 'set-config') {
74+
if (args[0] === 'default-package-manager' && args[1]) {
75+
try {
76+
setDefaultPackageManager(args[1]);
77+
process.exit(0);
78+
} catch (error) {
79+
console.error(error instanceof Error ? error.message : error);
80+
process.exit(1);
81+
}
82+
} else {
83+
console.error('Usage: xpm set-config default-package-manager <npm|yarn|pnpm|bun>');
84+
process.exit(1);
85+
}
86+
}
87+
6688
try {
6789
const { packageManager, projectRoot, isWorkspace, workspaceRoot } = detectPackageManager();
68-
const [command, ...args] = this.args;
6990

7091
// Auto-sync dependencies unless it's an install-like command or no command
7192
if (command && !skipInstallCommands.includes(command)) {

0 commit comments

Comments
 (0)