jpm-pkg 1.0.3 → 1.0.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/bin/jpm.js +7 -1
- package/package.json +1 -1
- package/src/commands/audit.js +70 -41
- package/src/commands/base-command.js +62 -0
- package/src/commands/config.js +94 -51
- package/src/commands/info.js +111 -64
- package/src/commands/init.js +117 -72
- package/src/commands/install.js +121 -112
- package/src/commands/list.js +126 -80
- package/src/commands/publish.js +155 -124
- package/src/commands/run.js +86 -52
- package/src/commands/search.js +56 -32
- package/src/commands/uninstall.js +47 -30
- package/src/commands/update.js +92 -51
- package/src/commands/x.js +134 -96
- package/src/core/cache.js +188 -87
- package/src/core/lockfile.js +73 -26
- package/src/core/package-json.js +119 -1
- package/src/core/registry.js +182 -143
- package/src/core/resolver.js +60 -36
package/src/commands/init.js
CHANGED
|
@@ -3,86 +3,131 @@
|
|
|
3
3
|
const readline = require('node:readline');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const os = require('node:os');
|
|
6
|
+
const BaseCommand = require('./base-command');
|
|
6
7
|
const PackageJSON = require('../core/package-json');
|
|
7
|
-
const logger = require('../utils/logger');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
pkg.save();
|
|
17
|
-
logger.success(`Created package.json`);
|
|
18
|
-
return;
|
|
9
|
+
/**
|
|
10
|
+
* InitCommand handles the 'jpm init' and 'jpm setup' commands.
|
|
11
|
+
* It initializes a new package.json file, either interactively or with defaults.
|
|
12
|
+
*/
|
|
13
|
+
class InitCommand extends BaseCommand {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('init');
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Executes the initialization process.
|
|
20
|
+
*
|
|
21
|
+
* @param {string[]} args - Optional arguments
|
|
22
|
+
* @param {Object} flags - CLI flags (e.g., -y for defaults)
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
async run(args, flags) {
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const pkgFile = path.join(cwd, 'package.json');
|
|
28
|
+
const pkg = new PackageJSON(pkgFile);
|
|
29
|
+
|
|
30
|
+
if (flags.y || flags.yes) {
|
|
31
|
+
// Non-interactive: write defaults immediately
|
|
32
|
+
pkg.save();
|
|
33
|
+
this.logger.success(`Created package.json`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.logger.section('JPM Init — create a new package.json');
|
|
38
|
+
this.logger.log(this.logger.c.gray('Press Enter to accept defaults shown in parentheses.\n'));
|
|
39
|
+
|
|
40
|
+
const existing = pkg.data;
|
|
41
|
+
|
|
42
|
+
const answers = await this._prompt([
|
|
43
|
+
{ key: 'name', label: 'Package name', default: existing.name || path.basename(cwd) },
|
|
44
|
+
{ key: 'version', label: 'Version', default: existing.version || '1.0.0' },
|
|
45
|
+
{ key: 'description', label: 'Description', default: existing.description || '' },
|
|
46
|
+
{ key: 'main', label: 'Entry point', default: existing.main || 'index.js' },
|
|
47
|
+
{ key: 'author', label: 'Author', default: existing.author || os.userInfo().username },
|
|
48
|
+
{ key: 'license', label: 'License', default: existing.license || 'MIT' },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const data = {
|
|
52
|
+
name: answers.name,
|
|
53
|
+
version: answers.version,
|
|
54
|
+
description: answers.description,
|
|
55
|
+
main: answers.main,
|
|
56
|
+
scripts: existing.scripts || { test: 'echo "Error: no test specified" && exit 1' },
|
|
57
|
+
keywords: existing.keywords || [],
|
|
58
|
+
author: answers.author,
|
|
59
|
+
license: answers.license,
|
|
60
|
+
dependencies: existing.dependencies || {},
|
|
61
|
+
devDependencies: existing.devDependencies || {},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Strip empty strings from output to keep it clean
|
|
65
|
+
for (const [k, v] of Object.entries(data)) {
|
|
66
|
+
if (v === '') delete data[k];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.logger.log('\n' + JSON.stringify(data, null, 2));
|
|
70
|
+
const confirm = await this._ask('\nIs this OK? (yes) ');
|
|
71
|
+
|
|
72
|
+
if (confirm.toLowerCase() === 'no' || confirm.toLowerCase() === 'n') {
|
|
73
|
+
this.logger.warn('Aborted.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [k, v] of Object.entries(data)) {
|
|
78
|
+
pkg.setField(k, v);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pkg.save();
|
|
82
|
+
this.logger.success(`\nWrote to ${pkgFile}`);
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Internal helper to prompt for multiple fields sequentially via terminal.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object[]} fields - Field definitions to prompt for
|
|
89
|
+
* @returns {Promise<Object>} Map of field keys to user answers
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_prompt(fields) {
|
|
93
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
94
|
+
const answers = {};
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
let i = 0;
|
|
97
|
+
const next = () => {
|
|
98
|
+
if (i >= fields.length) {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve(answers);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const f = fields[i++];
|
|
104
|
+
const label = this.logger.c.cyan(f.label) + (f.default ? this.logger.c.gray(` (${f.default})`) : '') + ': ';
|
|
105
|
+
rl.question(label, (val) => {
|
|
106
|
+
answers[f.key] = val.trim() || f.default || '';
|
|
107
|
+
next();
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
next();
|
|
111
|
+
});
|
|
58
112
|
}
|
|
59
113
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const label = logger.c.cyan(f.label) + (f.default ? logger.c.gray(` (${f.default})`) : '') + ': ';
|
|
74
|
-
rl.question(label, (val) => {
|
|
75
|
-
answers[f.key] = val.trim() || f.default || '';
|
|
76
|
-
next();
|
|
114
|
+
/**
|
|
115
|
+
* Internal helper to ask a single question via terminal.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} label - Question text
|
|
118
|
+
* @returns {Promise<string>} Trimmed user response
|
|
119
|
+
* @private
|
|
120
|
+
*/
|
|
121
|
+
_ask(label) {
|
|
122
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
return new Promise(resolve => {
|
|
124
|
+
rl.question(this.logger.c.cyan(label), (val) => {
|
|
125
|
+
rl.close();
|
|
126
|
+
resolve(val.trim());
|
|
77
127
|
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
81
130
|
}
|
|
82
131
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return new Promise(resolve => {
|
|
86
|
-
rl.question(logger.c.cyan(label), (val) => { rl.close(); resolve(val.trim()); });
|
|
87
|
-
});
|
|
88
|
-
}
|
|
132
|
+
module.exports = InitCommand;
|
|
133
|
+
|
package/src/commands/install.js
CHANGED
|
@@ -1,139 +1,148 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const BaseCommand = require('./base-command');
|
|
3
4
|
const Resolver = require('../core/resolver');
|
|
4
5
|
const Installer = require('../core/installer');
|
|
5
6
|
const Lockfile = require('../core/lockfile');
|
|
6
7
|
const PackageJSON = require('../core/package-json');
|
|
7
|
-
const registry = require('../core/registry');
|
|
8
|
-
const semver = require('../utils/semver');
|
|
9
8
|
const { Spinner } = require('../utils/progress');
|
|
10
|
-
const logger = require('../utils/logger');
|
|
11
|
-
const config = require('../utils/config');
|
|
12
9
|
|
|
13
10
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @param {string[]} args - Positional arguments (package names/versions)
|
|
18
|
-
* @param {Object} flags - CLI flags (e.g., --save-dev, --fast)
|
|
19
|
-
* @returns {Promise<void>}
|
|
11
|
+
* InstallCommand handles the 'jpm install' command and its aliases.
|
|
12
|
+
* It coordinates package resolution, downloading, and filesystem installation.
|
|
20
13
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
14
|
+
class InstallCommand extends BaseCommand {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('install');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Executes the installation process.
|
|
21
|
+
*
|
|
22
|
+
* @param {string[]} args - Positional arguments (package names/versions)
|
|
23
|
+
* @param {Object} flags - CLI flags (e.g., --save-dev, --fast)
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
async run(args, flags) {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const pkgJson = PackageJSON.fromDir(cwd);
|
|
29
|
+
const lockfile = new Lockfile(cwd);
|
|
30
|
+
const isDev = flags.D || flags['save-dev'];
|
|
31
|
+
const isExact = flags.E || flags['save-exact'] || this.config.saveExact;
|
|
32
|
+
const noSave = flags['no-save'];
|
|
33
|
+
const dryRun = flags['dry-run'];
|
|
34
|
+
|
|
35
|
+
// ── Parse packages to install ──────────────────────────────────────────────
|
|
36
|
+
let toInstall = [];
|
|
37
|
+
|
|
38
|
+
if (args.length) {
|
|
39
|
+
// Specific packages requested: `jpm install express lodash@4.17.21`
|
|
40
|
+
for (const arg of args) {
|
|
41
|
+
const lastAt = arg.lastIndexOf('@');
|
|
42
|
+
const hasVersion = lastAt > 0;
|
|
43
|
+
const name = hasVersion ? arg.slice(0, lastAt) : arg;
|
|
44
|
+
const version = hasVersion ? arg.slice(lastAt + 1) : 'latest';
|
|
45
|
+
toInstall.push({ name, version });
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// General install from package.json
|
|
49
|
+
const spinner = new Spinner('Reading package.json...').start();
|
|
50
|
+
const allDeps = {
|
|
51
|
+
...pkgJson.dependencies,
|
|
52
|
+
...(flags.production ? {} : pkgJson.devDependencies),
|
|
53
|
+
};
|
|
54
|
+
spinner.succeed(`Found ${Object.keys(allDeps).length} dependencies`);
|
|
55
|
+
|
|
56
|
+
// Deterministic install from lockfile if it exists and no arguments provided
|
|
57
|
+
if (lockfile.exists()) {
|
|
58
|
+
this.logger.info('Using lock file for deterministic install');
|
|
59
|
+
const lockData = lockfile.allPackages();
|
|
60
|
+
const installer = new Installer(cwd);
|
|
61
|
+
const bar = new Spinner('Installing from lock file...');
|
|
62
|
+
bar.start();
|
|
63
|
+
const fakeMap = new Map(lockData.map(p => [`${p.name}@${p.version}`, p]));
|
|
64
|
+
await installer.installAll(fakeMap, { dryRun, flags });
|
|
65
|
+
bar.succeed(`Installed ${lockData.length} packages`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toInstall = Object.entries(allDeps).map(([name, version]) => ({ name, version }));
|
|
41
70
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
...pkgJson.dependencies,
|
|
46
|
-
...(flags.production ? {} : pkgJson.devDependencies),
|
|
47
|
-
};
|
|
48
|
-
spinner.succeed(`Found ${Object.keys(allDeps).length} dependencies`);
|
|
49
|
-
|
|
50
|
-
// Prioritize lockfile-based installation for deterministic results
|
|
51
|
-
if (lockfile.exists()) {
|
|
52
|
-
logger.info('Using lock file for deterministic install');
|
|
53
|
-
const lockData = lockfile.allPackages();
|
|
54
|
-
const installer = new Installer(cwd);
|
|
55
|
-
const bar = new (require('../utils/progress').Spinner)('Installing from lock file...');
|
|
56
|
-
bar.start();
|
|
57
|
-
const fakeMap = new Map(lockData.map(p => [`${p.name}@${p.version}`, p]));
|
|
58
|
-
await installer.installAll(fakeMap, { dryRun, flags });
|
|
59
|
-
bar.succeed(`Installed ${lockData.length} packages`);
|
|
71
|
+
|
|
72
|
+
if (!toInstall.length) {
|
|
73
|
+
this.logger.info('Nothing to install.');
|
|
60
74
|
return;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
// ── Resolve ────────────────────────────────────────────────────────────────
|
|
78
|
+
const resolveSpinner = new Spinner(`Resolving ${toInstall.length} package(s)...`).start();
|
|
79
|
+
const resolver = new Resolver();
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
const deps = {};
|
|
82
|
+
const devDeps = {};
|
|
83
|
+
for (const { name, version } of toInstall) {
|
|
84
|
+
if (isDev) devDeps[name] = version;
|
|
85
|
+
else deps[name] = version;
|
|
86
|
+
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
let resolvedMap;
|
|
89
|
+
try {
|
|
90
|
+
resolvedMap = await resolver.resolve(deps, devDeps, {}, (count, total) => {
|
|
91
|
+
resolveSpinner.text = `Resolving packages... (${count} resolved)`;
|
|
92
|
+
});
|
|
93
|
+
resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages (including transitive deps)`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
resolveSpinner.fail(`Resolution failed: ${err.message}`);
|
|
96
|
+
throw err; // Let caller handle exit
|
|
97
|
+
}
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
99
|
+
// Warn about circular deps
|
|
100
|
+
const circular = resolver.findCircular();
|
|
101
|
+
if (circular.length) {
|
|
102
|
+
this.logger.warn(`Circular dependencies detected:\n ${circular.join('\n ')}`);
|
|
103
|
+
}
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
105
|
+
// ── Install ────────────────────────────────────────────────────────────────
|
|
106
|
+
this.logger.info(`Installing ${resolvedMap.size} packages...`);
|
|
107
|
+
const installer = new Installer(cwd);
|
|
108
|
+
try {
|
|
109
|
+
await installer.installAll(resolvedMap, { dryRun, flags });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
this.logger.error(`Install failed: ${err.message}`);
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
// ── Update package.json & lock file ─────────────────────────────────────────
|
|
116
|
+
if (!noSave && !dryRun && args.length) {
|
|
117
|
+
for (const { name } of toInstall) {
|
|
118
|
+
const resolved = [...resolvedMap.values()].find(m => m.name === name);
|
|
119
|
+
if (!resolved) continue;
|
|
120
|
+
pkgJson.addDependency(name, resolved.version, { dev: !!isDev, exact: isExact });
|
|
121
|
+
}
|
|
122
|
+
pkgJson.save();
|
|
123
|
+
this.logger.verbose('Updated package.json');
|
|
124
|
+
}
|
|
98
125
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await installer.installAll(resolvedMap, { dryRun, flags });
|
|
104
|
-
} catch (err) {
|
|
105
|
-
logger.error(`Install failed: ${err.message}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
126
|
+
if (!dryRun) {
|
|
127
|
+
lockfile.update(resolvedMap).save();
|
|
128
|
+
this.logger.verbose('Updated jpm-lock.json');
|
|
129
|
+
}
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const resolved = [...resolvedMap.values()].find(m => m.name === name);
|
|
114
|
-
if (!resolved) continue;
|
|
115
|
-
pkgJson.addDependency(name, resolved.version, { dev: !!isDev, exact: isExact });
|
|
131
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
132
|
+
const dupes = resolver.deduplicate();
|
|
133
|
+
if (dupes.length) {
|
|
134
|
+
this.logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
|
|
116
135
|
}
|
|
117
|
-
pkgJson.save();
|
|
118
|
-
logger.verbose('Updated package.json');
|
|
119
|
-
}
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
const directly = toInstall.map(({ name }) => {
|
|
138
|
+
const r = [...resolvedMap.values()].find(m => m.name === name);
|
|
139
|
+
return r ? `${r.name}@${r.version}` : name;
|
|
140
|
+
});
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (dupes.length) {
|
|
129
|
-
logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
|
|
142
|
+
this.logger.success(`\nadded ${resolvedMap.size} packages`);
|
|
143
|
+
directly.forEach(p => this.logger.log(` + ${this.logger.c.green(p)}`));
|
|
130
144
|
}
|
|
145
|
+
}
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
const r = [...resolvedMap.values()].find(m => m.name === name);
|
|
134
|
-
return r ? `${r.name}@${r.version}` : name;
|
|
135
|
-
});
|
|
147
|
+
module.exports = InstallCommand;
|
|
136
148
|
|
|
137
|
-
logger.success(`\nadded ${resolvedMap.size} packages`);
|
|
138
|
-
directly.forEach(p => logger.log(` + ${logger.c.green(p)}`));
|
|
139
|
-
};
|