jpm-pkg 1.0.3

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.
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const readline = require('node:readline');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const PackageJSON = require('../core/package-json');
7
+ const logger = require('../utils/logger');
8
+
9
+ module.exports = async function init(args, flags) {
10
+ const cwd = process.cwd();
11
+ const pkgFile = path.join(cwd, 'package.json');
12
+ const pkg = new PackageJSON(pkgFile);
13
+
14
+ if (flags.y || flags.yes) {
15
+ // Non-interactive: write defaults
16
+ pkg.save();
17
+ logger.success(`Created package.json`);
18
+ return;
19
+ }
20
+
21
+ logger.section('JPM Init — create a new package.json');
22
+ logger.log(logger.c.gray('Press Enter to accept defaults shown in parentheses.\n'));
23
+
24
+ const existing = pkg.data;
25
+
26
+ const answers = await prompt([
27
+ { key: 'name', label: 'Package name', default: existing.name || path.basename(cwd) },
28
+ { key: 'version', label: 'Version', default: existing.version || '1.0.0' },
29
+ { key: 'description', label: 'Description', default: existing.description || '' },
30
+ { key: 'main', label: 'Entry point', default: existing.main || 'index.js' },
31
+ { key: 'author', label: 'Author', default: existing.author || os.userInfo().username },
32
+ { key: 'license', label: 'License', default: existing.license || 'MIT' },
33
+ ]);
34
+
35
+ const data = {
36
+ name: answers.name,
37
+ version: answers.version,
38
+ description: answers.description,
39
+ main: answers.main,
40
+ scripts: existing.scripts || { test: 'echo "Error: no test specified" && exit 1' },
41
+ keywords: existing.keywords || [],
42
+ author: answers.author,
43
+ license: answers.license,
44
+ dependencies: existing.dependencies || {},
45
+ devDependencies: existing.devDependencies || {},
46
+ };
47
+
48
+ // Strip empty strings from output
49
+ for (const [k, v] of Object.entries(data)) {
50
+ if (v === '') delete data[k];
51
+ }
52
+
53
+ logger.log('\n' + JSON.stringify(data, null, 2));
54
+ const confirm = await ask('\nIs this OK? (yes) ');
55
+ if (confirm === 'no' || confirm === 'n') {
56
+ logger.warn('Aborted.');
57
+ return;
58
+ }
59
+
60
+ for (const [k, v] of Object.entries(data)) pkg.setField(k, v);
61
+ pkg.save();
62
+ logger.success(`\nWrote to ${pkgFile}`);
63
+ };
64
+
65
+ function prompt(fields) {
66
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
+ const answers = {};
68
+ return new Promise(resolve => {
69
+ let i = 0;
70
+ function next() {
71
+ if (i >= fields.length) { rl.close(); resolve(answers); return; }
72
+ const f = fields[i++];
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();
77
+ });
78
+ }
79
+ next();
80
+ });
81
+ }
82
+
83
+ function ask(label) {
84
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
85
+ return new Promise(resolve => {
86
+ rl.question(logger.c.cyan(label), (val) => { rl.close(); resolve(val.trim()); });
87
+ });
88
+ }
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const Resolver = require('../core/resolver');
4
+ const Installer = require('../core/installer');
5
+ const Lockfile = require('../core/lockfile');
6
+ const PackageJSON = require('../core/package-json');
7
+ const registry = require('../core/registry');
8
+ const semver = require('../utils/semver');
9
+ const { Spinner } = require('../utils/progress');
10
+ const logger = require('../utils/logger');
11
+ const config = require('../utils/config');
12
+
13
+ /**
14
+ * Command handler for 'jpm install' (and its aliases 'get', 'add', 'syn').
15
+ * Orchestrates package resolution, downloading, and filesystem installation.
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>}
20
+ */
21
+ module.exports = async function install(args, flags) {
22
+ const cwd = process.cwd();
23
+ const pkgJson = PackageJSON.fromDir(cwd);
24
+ const lockfile = new Lockfile(cwd);
25
+ const isDev = flags.D || flags['save-dev'];
26
+ const isExact = flags.E || flags['save-exact'] || config.saveExact;
27
+ const noSave = flags['no-save'];
28
+ const dryRun = flags['dry-run'];
29
+
30
+ // ── Parse packages to install ──────────────────────────────────────────────
31
+ let toInstall = [];
32
+
33
+ if (args.length) {
34
+ // `jpm install express lodash@4.17.21 @types/node`
35
+ for (const arg of args) {
36
+ const lastAt = arg.lastIndexOf('@');
37
+ const hasVersion = lastAt > 0;
38
+ const name = hasVersion ? arg.slice(0, lastAt) : arg;
39
+ const version = hasVersion ? arg.slice(lastAt + 1) : 'latest';
40
+ toInstall.push({ name, version });
41
+ }
42
+ } else {
43
+ const spinner = new Spinner('Reading package.json...').start();
44
+ const allDeps = {
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`);
60
+ return;
61
+ }
62
+
63
+ toInstall = Object.entries(allDeps).map(([name, version]) => ({ name, version }));
64
+ }
65
+
66
+ if (!toInstall.length) {
67
+ logger.info('Nothing to install.');
68
+ return;
69
+ }
70
+
71
+ // ── Resolve ────────────────────────────────────────────────────────────────
72
+ const resolveSpinner = new Spinner(`Resolving ${toInstall.length} package(s)...`).start();
73
+ const resolver = new Resolver();
74
+
75
+ const deps = {};
76
+ const devDeps = {};
77
+ for (const { name, version } of toInstall) {
78
+ if (isDev) devDeps[name] = version;
79
+ else deps[name] = version;
80
+ }
81
+
82
+ let resolvedMap;
83
+ try {
84
+ resolvedMap = await resolver.resolve(deps, devDeps, {}, (count, total) => {
85
+ resolveSpinner.text = `Resolving packages... (${count} resolved)`;
86
+ });
87
+ resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages (including transitive deps)`);
88
+ } catch (err) {
89
+ resolveSpinner.fail(`Resolution failed: ${err.message}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ // Warn about circular deps
94
+ const circular = resolver.findCircular();
95
+ if (circular.length) {
96
+ logger.warn(`Circular dependencies detected:\n ${circular.join('\n ')}`);
97
+ }
98
+
99
+ // ── Install ────────────────────────────────────────────────────────────────
100
+ logger.info(`Installing ${resolvedMap.size} packages...`);
101
+ const installer = new Installer(cwd);
102
+ try {
103
+ await installer.installAll(resolvedMap, { dryRun, flags });
104
+ } catch (err) {
105
+ logger.error(`Install failed: ${err.message}`);
106
+ process.exit(1);
107
+ }
108
+
109
+ // ── Update package.json & lock file ─────────────────────────────────────────
110
+ if (!noSave && !dryRun && args.length) {
111
+ for (const { name } of toInstall) {
112
+ // Find actual resolved version
113
+ const resolved = [...resolvedMap.values()].find(m => m.name === name);
114
+ if (!resolved) continue;
115
+ pkgJson.addDependency(name, resolved.version, { dev: !!isDev, exact: isExact });
116
+ }
117
+ pkgJson.save();
118
+ logger.verbose('Updated package.json');
119
+ }
120
+
121
+ if (!dryRun) {
122
+ lockfile.update(resolvedMap).save();
123
+ logger.verbose('Updated jpm-lock.json');
124
+ }
125
+
126
+ // ── Summary ────────────────────────────────────────────────────────────────
127
+ const dupes = resolver.deduplicate();
128
+ if (dupes.length) {
129
+ logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
130
+ }
131
+
132
+ const directly = toInstall.map(({ name }) => {
133
+ const r = [...resolvedMap.values()].find(m => m.name === name);
134
+ return r ? `${r.name}@${r.version}` : name;
135
+ });
136
+
137
+ logger.success(`\nadded ${resolvedMap.size} packages`);
138
+ directly.forEach(p => logger.log(` + ${logger.c.green(p)}`));
139
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const PackageJSON = require('../core/package-json');
6
+ const { formatBytes } = require('../utils/fs');
7
+ const logger = require('../utils/logger');
8
+
9
+ module.exports = async function list(args, flags) {
10
+ const cwd = process.cwd();
11
+ const depth = parseInt(flags.depth ?? flags.d ?? '0', 10);
12
+ const json = flags.json;
13
+ const nodeModules = path.join(cwd, 'node_modules');
14
+
15
+ const pkgJson = PackageJSON.fromDir(cwd);
16
+
17
+ if (!fs.existsSync(nodeModules)) {
18
+ logger.warn('No node_modules found. Run `jpm install` first.');
19
+ return;
20
+ }
21
+
22
+ // Collect top-level installed packages
23
+ const installed = [];
24
+ for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) {
25
+ if (entry.name.startsWith('.')) continue;
26
+
27
+ if (entry.name.startsWith('@') && entry.isDirectory()) {
28
+ const scopeDir = path.join(nodeModules, entry.name);
29
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
30
+ const pkg = readPkg(path.join(scopeDir, scoped.name));
31
+ if (pkg) installed.push(pkg);
32
+ }
33
+ } else if (entry.isDirectory()) {
34
+ const pkg = readPkg(path.join(nodeModules, entry.name));
35
+ if (pkg) installed.push(pkg);
36
+ }
37
+ }
38
+
39
+ // Sort alphabetically
40
+ installed.sort((a, b) => a.name.localeCompare(b.name));
41
+
42
+ if (json) {
43
+ process.stdout.write(JSON.stringify({ name: pkgJson.name, dependencies: toObj(installed) }, null, 2) + '\n');
44
+ return;
45
+ }
46
+
47
+ logger.log(`\n${logger.c.bold(pkgJson.name)}@${pkgJson.version}`);
48
+
49
+ const directDeps = new Set([
50
+ ...Object.keys(pkgJson.dependencies),
51
+ ...Object.keys(pkgJson.devDependencies),
52
+ ]);
53
+
54
+ const total = installed.length;
55
+ let totalSize = 0;
56
+
57
+ for (let i = 0; i < installed.length; i++) {
58
+ const pkg = installed[i];
59
+ const isLast = i === installed.length - 1;
60
+ const isDir = directDeps.has(pkg.name);
61
+ const devMark = Object.keys(pkgJson.devDependencies).includes(pkg.name)
62
+ ? logger.c.gray(' dev')
63
+ : '';
64
+
65
+ const conn = isLast ? '└── ' : '├── ';
66
+ const nameStr = logger.c.cyan(pkg.name);
67
+ const verStr = logger.c.gray(`@${pkg.version}`);
68
+ logger.log(`${conn}${nameStr}${verStr}${devMark}`);
69
+
70
+ if (depth > 0 && pkg.dependencies) {
71
+ const subDeps = Object.entries(pkg.dependencies);
72
+ subDeps.forEach(([depName, depRange], j) => {
73
+ const isLastSub = j === subDeps.length - 1;
74
+ const ext = isLast ? ' ' : '│ ';
75
+ const subConn = isLastSub ? '└── ' : '├── ';
76
+ logger.log(`${ext}${subConn}${logger.c.gray(depName)} ${logger.c.gray(depRange)}`);
77
+ });
78
+ }
79
+
80
+ totalSize += pkg.size || 0;
81
+ }
82
+
83
+ logger.log(`\n${total} packages ${formatBytes(totalSize)}`);
84
+ };
85
+
86
+ function readPkg(dir) {
87
+ const f = path.join(dir, 'package.json');
88
+ try {
89
+ const data = JSON.parse(fs.readFileSync(f, 'utf8'));
90
+ let size = 0;
91
+ try {
92
+ for (const file of fs.readdirSync(dir)) {
93
+ const s = fs.statSync(path.join(dir, file));
94
+ if (s.isFile()) size += s.size;
95
+ }
96
+ } catch { }
97
+ return { name: data.name, version: data.version, dependencies: data.dependencies, size };
98
+ } catch { return null; }
99
+ }
100
+
101
+ function toObj(arr) {
102
+ return Object.fromEntries(arr.map(p => [p.name, { version: p.version }]));
103
+ }
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const tar = require('tar');
7
+ const PackageJSON = require('../core/package-json');
8
+ const integrity = require('../security/integrity');
9
+ const { getJSON } = require('../utils/http');
10
+ const { tempDir, mkdirp } = require('../utils/fs');
11
+ const { Spinner } = require('../utils/progress');
12
+ const logger = require('../utils/logger');
13
+ const config = require('../utils/config');
14
+
15
+ module.exports = async function publish(args, flags) {
16
+ const cwd = process.cwd();
17
+ const pkgJson = PackageJSON.fromDir(cwd);
18
+
19
+ // 1. Validate
20
+ const errors = pkgJson.validate();
21
+ if (errors.length) {
22
+ logger.error('package.json validation failed:');
23
+ errors.forEach(e => logger.error(` ${e}`));
24
+ process.exit(1);
25
+ }
26
+
27
+ const { name, version } = pkgJson;
28
+ const registry = config.registry.replace(/\/$/, '');
29
+
30
+ logger.section(`Publishing ${name}@${version}`);
31
+
32
+ // 2. Check auth token
33
+ const token = config.get('//registry.npmjs.org/:_authToken') || config.get('_authToken');
34
+ if (!token) {
35
+ logger.error('Not logged in. Set _authToken in ~/.jpmrc or run: npm login');
36
+ process.exit(1);
37
+ }
38
+
39
+ // 3. Check if version already exists
40
+ const spinner = new Spinner('Checking if version exists...').start();
41
+ try {
42
+ const existing = await getJSON(`${registry}/${encodeURIComponent(name)}/${version}`);
43
+ spinner.fail(`Version ${name}@${version} already exists in registry`);
44
+ process.exit(1);
45
+ } catch (err) {
46
+ if (err.status !== 404) {
47
+ spinner.warn('Could not verify version uniqueness, continuing...');
48
+ } else {
49
+ spinner.succeed('Version check passed');
50
+ }
51
+ }
52
+
53
+ // 4. Create tarball
54
+ const tmp = tempDir('jpm-publish-');
55
+ const tgz = path.join(tmp, `${name.replace('@', '').replace('/', '-')}-${version}.tgz`);
56
+
57
+ const packSpinner = new Spinner('Packing tarball...').start();
58
+
59
+ // Collect files respecting .npmignore / default ignore list
60
+ const ignore = getIgnoreList(cwd);
61
+ await tar.create(
62
+ { gzip: true, file: tgz, cwd, prefix: 'package' },
63
+ getFilesToPack(cwd, ignore)
64
+ );
65
+
66
+ const tgzStat = fs.statSync(tgz);
67
+ const shasum = await integrity.hashFile(tgz, 'sha1', 'hex');
68
+ const integrityH = await integrity.generateIntegrity(tgz);
69
+ const tgzBase64 = fs.readFileSync(tgz).toString('base64');
70
+
71
+ packSpinner.succeed(`Packed ${logger.c.gray(`(${(tgzStat.size / 1024).toFixed(1)} KB)`)}`);
72
+
73
+ // 5. Build publish body
74
+ const body = {
75
+ _id: name,
76
+ name,
77
+ 'dist-tags': { latest: version },
78
+ versions: {
79
+ [version]: {
80
+ ...pkgJson.data,
81
+ dist: {
82
+ shasum,
83
+ integrity: integrityH,
84
+ tarball: `${registry}/${encodeURIComponent(name)}/-/${name}-${version}.tgz`,
85
+ },
86
+ },
87
+ },
88
+ _attachments: {
89
+ [`${name}-${version}.tgz`]: {
90
+ content_type: 'application/octet-stream',
91
+ data: tgzBase64,
92
+ length: tgzStat.size,
93
+ },
94
+ },
95
+ };
96
+
97
+ // 6. PUT to registry
98
+ const uploadSpinner = new Spinner('Uploading to registry...').start();
99
+ const { request } = require('../utils/http');
100
+ const bodyStr = JSON.stringify(body);
101
+
102
+ try {
103
+ const res = await request(`${registry}/${encodeURIComponent(name)}`, {
104
+ method: 'PUT',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'Authorization': `Bearer ${token}`,
108
+ 'Content-Length': Buffer.byteLength(bodyStr),
109
+ ...(flags.otp ? { 'npm-otp': flags.otp } : {}),
110
+ },
111
+ body: bodyStr,
112
+ retries: 1,
113
+ });
114
+
115
+ if (res.status < 200 || res.status >= 300) {
116
+ uploadSpinner.fail(`Publish failed: HTTP ${res.status} — ${res.body.slice(0, 200)}`);
117
+ process.exit(1);
118
+ }
119
+
120
+ uploadSpinner.succeed('Published!');
121
+ logger.success(`\n+ ${name}@${version}`);
122
+ logger.log(logger.c.gray(`${registry}/${encodeURIComponent(name)}`));
123
+ } catch (err) {
124
+ uploadSpinner.fail(`Publish error: ${err.message}`);
125
+ process.exit(1);
126
+ }
127
+ };
128
+
129
+ function getIgnoreList(dir) {
130
+ const defaults = new Set(['node_modules', '.git', '.DS_Store', '*.log', 'coverage', '.jpm-lock.json']);
131
+ const ignoreFile = path.join(dir, '.npmignore');
132
+ if (fs.existsSync(ignoreFile)) {
133
+ fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(l => {
134
+ const t = l.trim();
135
+ if (t && !t.startsWith('#')) defaults.add(t);
136
+ });
137
+ }
138
+ return defaults;
139
+ }
140
+
141
+ function getFilesToPack(dir, ignore) {
142
+ const files = [];
143
+ for (const entry of fs.readdirSync(dir)) {
144
+ if ([...ignore].some(ig => entry === ig || entry.startsWith(ig.replace('*', '')))) continue;
145
+ files.push(entry);
146
+ }
147
+ return files.length ? files : ['.'];
148
+ }
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+ const path = require('node:path');
5
+ const PackageJSON = require('../core/package-json');
6
+ const logger = require('../utils/logger');
7
+
8
+ module.exports = async function run(args, flags) {
9
+ const [scriptName, ...scriptArgs] = args;
10
+ const cwd = process.cwd();
11
+ const pkgJson = PackageJSON.fromDir(cwd);
12
+ const scripts = pkgJson.scripts;
13
+
14
+ // `jpm run` with no script — list available scripts
15
+ if (!scriptName) {
16
+ logger.section('Available scripts:');
17
+ if (!Object.keys(scripts).length) {
18
+ logger.info('No scripts defined in package.json');
19
+ return;
20
+ }
21
+ for (const [name, cmd] of Object.entries(scripts)) {
22
+ logger.log(` ${logger.c.cyan(name.padEnd(20))} ${logger.c.gray(cmd)}`);
23
+ }
24
+ return;
25
+ }
26
+
27
+ if (!scripts[scriptName]) {
28
+ logger.error(`Script "${scriptName}" not found in package.json`);
29
+ logger.info(`Available: ${Object.keys(scripts).join(', ')}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ // Pre-hook: check for 'pre<script>'
34
+ if (scripts[`pre${scriptName}`]) {
35
+ logger.info(`> ${pkgJson.name} pre${scriptName}`);
36
+ run_script(`pre${scriptName}`, scripts, cwd, scriptArgs);
37
+ }
38
+
39
+ logger.info(`\n> ${pkgJson.name} ${scriptName}`);
40
+ logger.info(`> ${scripts[scriptName]}\n`);
41
+
42
+ const result = run_script(scriptName, scripts, cwd, scriptArgs);
43
+
44
+ // Post-hook: check for 'post<script>'
45
+ if (scripts[`post${scriptName}`]) {
46
+ logger.info(`> ${pkgJson.name} post${scriptName}`);
47
+ run_script(`post${scriptName}`, scripts, cwd, scriptArgs);
48
+ }
49
+
50
+ if (result.status !== 0) {
51
+ logger.error(`Script "${scriptName}" exited with code ${result.status}`);
52
+ process.exit(result.status || 1);
53
+ }
54
+ };
55
+
56
+ function run_script(name, scripts, cwd, extraArgs) {
57
+ const cmd = scripts[name] + (extraArgs.length ? ' ' + extraArgs.join(' ') : '');
58
+
59
+ // Add local .bin to PATH so locally installed binaries work
60
+ const binPath = path.join(cwd, 'node_modules', '.bin');
61
+ const env = {
62
+ ...process.env,
63
+ PATH: `${binPath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`,
64
+ };
65
+
66
+ return spawnSync(cmd, {
67
+ cwd,
68
+ shell: true,
69
+ stdio: 'inherit',
70
+ env,
71
+ });
72
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const registry = require('../core/registry');
4
+ const { Spinner } = require('../utils/progress');
5
+ const logger = require('../utils/logger');
6
+
7
+ module.exports = async function search(args, flags) {
8
+ const query = args.join(' ');
9
+ if (!query) { logger.error('Usage: jpm search <query>'); process.exit(1); }
10
+
11
+ const size = parseInt(flags.size || flags.n || '20', 10);
12
+ const spinner = new Spinner(`Searching for "${query}"...`).start();
13
+
14
+ let results;
15
+ try {
16
+ results = await registry.search(query, { size });
17
+ } catch (err) {
18
+ spinner.fail(`Search failed: ${err.message}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ spinner.succeed(`Found ${results.length} result(s) for "${query}"`);
23
+
24
+ if (!results.length) { logger.info('No packages found.'); return; }
25
+
26
+ logger.log('');
27
+ const rows = results.map(r => ({
28
+ Name: r.package?.name || '',
29
+ Version: r.package?.version || '',
30
+ Description: (r.package?.description || '').slice(0, 55),
31
+ Downloads: r.downloads?.weekly != null
32
+ ? r.downloads.weekly.toLocaleString()
33
+ : 'n/a',
34
+ Score: r.score?.final != null
35
+ ? (r.score.final * 100).toFixed(0) + '%'
36
+ : 'n/a',
37
+ }));
38
+ logger.table(rows, ['Name', 'Version', 'Description', 'Downloads', 'Score']);
39
+
40
+ logger.log(`\n${logger.c.gray('Run `jpm info <package>` for details. `jpm install <package>` to install.')}`);
41
+ };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const PackageJSON = require('../core/package-json');
4
+ const Installer = require('../core/installer');
5
+ const Lockfile = require('../core/lockfile');
6
+ const Resolver = require('../core/resolver');
7
+ const { Spinner } = require('../utils/progress');
8
+ const logger = require('../utils/logger');
9
+
10
+ module.exports = async function uninstall(args, flags) {
11
+ if (!args.length) {
12
+ logger.error('Usage: jpm uninstall <package> [package2 ...]');
13
+ process.exit(1);
14
+ }
15
+
16
+ const cwd = process.cwd();
17
+ const pkgJson = PackageJSON.fromDir(cwd);
18
+ const lockfile = new Lockfile(cwd);
19
+ const installer = new Installer(cwd);
20
+
21
+ for (const name of args) {
22
+ const spinner = new Spinner(`Removing ${name}...`).start();
23
+
24
+ const removed = await installer.uninstall(name);
25
+ if (!removed) {
26
+ spinner.warn(`${name} was not installed`);
27
+ continue;
28
+ }
29
+
30
+ // Remove from package.json
31
+ pkgJson.removeDependency(name);
32
+
33
+ // Remove from lock file — remove all entries with this name
34
+ const toRemove = lockfile.allPackages()
35
+ .filter(p => p.name === name)
36
+ .map(p => ({ name: p.name, version: p.version }));
37
+
38
+ for (const { name: n, version: v } of toRemove) {
39
+ lockfile.removePackage(n, v);
40
+ }
41
+
42
+ spinner.succeed(`Removed ${name}`);
43
+ }
44
+
45
+ pkgJson.save();
46
+ lockfile.save();
47
+ logger.success(`\nRemoved ${args.length} package(s)`);
48
+ };