jpm-pkg 1.0.3 → 1.0.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.
@@ -2,71 +2,105 @@
2
2
 
3
3
  const { spawnSync } = require('node:child_process');
4
4
  const path = require('node:path');
5
+ const BaseCommand = require('./base-command');
5
6
  const PackageJSON = require('../core/package-json');
6
- const logger = require('../utils/logger');
7
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;
8
+ /**
9
+ * RunCommand handles the 'jpm run' command.
10
+ * It executes scripts defined in package.json, supporting pre/post hooks.
11
+ */
12
+ class RunCommand extends BaseCommand {
13
+ constructor() {
14
+ super('run');
15
+ }
16
+
17
+ /**
18
+ * Executes a specified script from package.json.
19
+ *
20
+ * @param {string[]} args - Positional arguments (script name and optional parameters)
21
+ * @param {Object} flags - CLI flags
22
+ * @returns {Promise<void>}
23
+ */
24
+ async run(args, flags) {
25
+ const [scriptName, ...scriptArgs] = args;
26
+ const cwd = process.cwd();
27
+ const pkgJson = PackageJSON.fromDir(cwd);
28
+ const scripts = pkgJson.scripts;
13
29
 
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');
30
+ // If no script name provided, list all available scripts
31
+ if (!scriptName) {
32
+ this.logger.section('Available scripts:');
33
+ const scriptEntries = Object.entries(scripts);
34
+ if (!scriptEntries.length) {
35
+ this.logger.info('No scripts defined in package.json');
36
+ return;
37
+ }
38
+ for (const [name, cmd] of scriptEntries) {
39
+ this.logger.log(` ${this.logger.c.cyan(name.padEnd(20))} ${this.logger.c.gray(cmd)}`);
40
+ }
19
41
  return;
20
42
  }
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
43
 
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
- }
44
+ if (!scripts[scriptName]) {
45
+ this.logger.error(`Script "${scriptName}" not found in package.json`);
46
+ this.logger.info(`Available: ${Object.keys(scripts).join(', ')}`);
47
+ throw new Error(`Script "${scriptName}" not found.`);
48
+ }
32
49
 
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
- }
50
+ // Execute pre-hook if it exists
51
+ const preHook = `pre${scriptName}`;
52
+ if (scripts[preHook]) {
53
+ this.logger.info(`> ${pkgJson.name} ${preHook}`);
54
+ this._executeScript(preHook, scripts, cwd, scriptArgs);
55
+ }
38
56
 
39
- logger.info(`\n> ${pkgJson.name} ${scriptName}`);
40
- logger.info(`> ${scripts[scriptName]}\n`);
57
+ this.logger.info(`\n> ${pkgJson.name} ${scriptName}`);
58
+ this.logger.info(`> ${scripts[scriptName]}\n`);
41
59
 
42
- const result = run_script(scriptName, scripts, cwd, scriptArgs);
60
+ const result = this._executeScript(scriptName, scripts, cwd, scriptArgs);
43
61
 
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
- }
62
+ // Execute post-hook if it exists
63
+ const postHook = `post${scriptName}`;
64
+ if (scripts[postHook]) {
65
+ this.logger.info(`> ${pkgJson.name} ${postHook}`);
66
+ this._executeScript(postHook, scripts, cwd, scriptArgs);
67
+ }
49
68
 
50
- if (result.status !== 0) {
51
- logger.error(`Script "${scriptName}" exited with code ${result.status}`);
52
- process.exit(result.status || 1);
69
+ if (result.status !== 0) {
70
+ const code = result.status || 1;
71
+ this.logger.error(`Script "${scriptName}" exited with code ${code}`);
72
+ process.exit(code);
73
+ }
53
74
  }
54
- };
55
75
 
56
- function run_script(name, scripts, cwd, extraArgs) {
57
- const cmd = scripts[name] + (extraArgs.length ? ' ' + extraArgs.join(' ') : '');
76
+ /**
77
+ * Internal helper to spawn a process for a script.
78
+ * Adds node_modules/.bin to the PATH.
79
+ *
80
+ * @param {string} name - Script name to run
81
+ * @param {Object} scripts - Script definitions from package.json
82
+ * @param {string} cwd - Current working directory
83
+ * @param {string[]} extraArgs - Additional arguments to pass to the script
84
+ * @returns {import('node:child_process').SpawnSyncReturns<Buffer>}
85
+ * @private
86
+ */
87
+ _executeScript(name, scripts, cwd, extraArgs) {
88
+ const cmd = scripts[name] + (extraArgs.length ? ' ' + extraArgs.join(' ') : '');
58
89
 
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
- };
90
+ const binPath = path.join(cwd, 'node_modules', '.bin');
91
+ const env = {
92
+ ...process.env,
93
+ PATH: `${binPath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`,
94
+ };
65
95
 
66
- return spawnSync(cmd, {
67
- cwd,
68
- shell: true,
69
- stdio: 'inherit',
70
- env,
71
- });
96
+ return spawnSync(cmd, {
97
+ cwd,
98
+ shell: true,
99
+ stdio: 'inherit',
100
+ env,
101
+ });
102
+ }
72
103
  }
104
+
105
+ module.exports = RunCommand;
106
+
@@ -1,41 +1,65 @@
1
1
  'use strict';
2
2
 
3
+ const BaseCommand = require('./base-command');
3
4
  const registry = require('../core/registry');
4
5
  const { Spinner } = require('../utils/progress');
5
- const logger = require('../utils/logger');
6
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); }
7
+ /**
8
+ * SearchCommand handles the 'jpm search' and 'jpm find' commands.
9
+ * It queries the registry for packages matching a search term and displays results.
10
+ */
11
+ class SearchCommand extends BaseCommand {
12
+ constructor() {
13
+ super('search');
14
+ }
15
+
16
+ /**
17
+ * Executes the package search.
18
+ *
19
+ * @param {string[]} args - Search query terms
20
+ * @param {Object} flags - CLI flags (e.g., --size)
21
+ * @returns {Promise<void>}
22
+ */
23
+ async run(args, flags) {
24
+ const query = args.join(' ');
25
+ if (!query) {
26
+ this.logger.error('Usage: jpm search <query>');
27
+ throw new Error('No search query provided.');
28
+ }
29
+
30
+ const size = parseInt(flags.size || flags.n || '20', 10);
31
+ const spinner = new Spinner(`Searching for "${query}"...`).start();
10
32
 
11
- const size = parseInt(flags.size || flags.n || '20', 10);
12
- const spinner = new Spinner(`Searching for "${query}"...`).start();
33
+ try {
34
+ const results = await registry.search(query, { size });
35
+ spinner.succeed(`Found ${results.length} result(s) for "${query}"`);
13
36
 
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);
37
+ if (!results.length) {
38
+ this.logger.info('No packages found.');
39
+ return;
40
+ }
41
+
42
+ this.logger.log('');
43
+ const rows = results.map(r => ({
44
+ Name: r.package?.name || '',
45
+ Version: r.package?.version || '',
46
+ Description: (r.package?.description || '').slice(0, 55),
47
+ Downloads: r.downloads?.weekly != null
48
+ ? r.downloads.weekly.toLocaleString()
49
+ : 'n/a',
50
+ Score: r.score?.final != null
51
+ ? (r.score.final * 100).toFixed(0) + '%'
52
+ : 'n/a',
53
+ }));
54
+
55
+ this.logger.table(rows, ['Name', 'Version', 'Description', 'Downloads', 'Score']);
56
+ this.logger.log(`\n${this.logger.c.gray('Run `jpm info <package>` for details. `jpm install <package>` to install.')}`);
57
+ } catch (err) {
58
+ spinner.fail(`Search failed: ${err.message}`);
59
+ throw err;
60
+ }
20
61
  }
62
+ }
63
+
64
+ module.exports = SearchCommand;
21
65
 
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
- };
@@ -1,48 +1,65 @@
1
1
  'use strict';
2
2
 
3
+ const BaseCommand = require('./base-command');
3
4
  const PackageJSON = require('../core/package-json');
4
5
  const Installer = require('../core/installer');
5
6
  const Lockfile = require('../core/lockfile');
6
- const Resolver = require('../core/resolver');
7
7
  const { Spinner } = require('../utils/progress');
8
- const logger = require('../utils/logger');
9
8
 
10
- module.exports = async function uninstall(args, flags) {
11
- if (!args.length) {
12
- logger.error('Usage: jpm uninstall <package> [package2 ...]');
13
- process.exit(1);
9
+ /**
10
+ * UninstallCommand handles the 'jpm uninstall' command and its aliases.
11
+ * It removes packages from node_modules and updates package.json and the lockfile.
12
+ */
13
+ class UninstallCommand extends BaseCommand {
14
+ constructor() {
15
+ super('uninstall');
14
16
  }
15
17
 
16
- const cwd = process.cwd();
17
- const pkgJson = PackageJSON.fromDir(cwd);
18
- const lockfile = new Lockfile(cwd);
19
- const installer = new Installer(cwd);
18
+ /**
19
+ * Executes the uninstallation process for one or more packages.
20
+ *
21
+ * @param {string[]} args - Names of packages to uninstall
22
+ * @param {Object} flags - CLI flags
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ if (!args.length) {
27
+ this.logger.error('Usage: jpm uninstall <package> [package2 ...]');
28
+ throw new Error('No package names provided for uninstallation.');
29
+ }
20
30
 
21
- for (const name of args) {
22
- const spinner = new Spinner(`Removing ${name}...`).start();
31
+ const cwd = process.cwd();
32
+ const pkgJson = PackageJSON.fromDir(cwd);
33
+ const lockfile = new Lockfile(cwd);
34
+ const installer = new Installer(cwd);
23
35
 
24
- const removed = await installer.uninstall(name);
25
- if (!removed) {
26
- spinner.warn(`${name} was not installed`);
27
- continue;
28
- }
36
+ for (const name of args) {
37
+ const spinner = new Spinner(`Removing ${name}...`).start();
38
+
39
+ const removed = await installer.uninstall(name);
40
+ if (!removed) {
41
+ spinner.warn(`${name} was not installed`);
42
+ continue;
43
+ }
29
44
 
30
- // Remove from package.json
31
- pkgJson.removeDependency(name);
45
+ // Remove from package.json
46
+ pkgJson.removeDependency(name);
32
47
 
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 }));
48
+ // Remove from lock file (remove all versions for this package name)
49
+ const lockedPackages = lockfile.allPackages();
50
+ for (const pkg of lockedPackages) {
51
+ if (pkg.name === name) {
52
+ lockfile.removePackage(pkg.name, pkg.version);
53
+ }
54
+ }
37
55
 
38
- for (const { name: n, version: v } of toRemove) {
39
- lockfile.removePackage(n, v);
56
+ spinner.succeed(`Removed ${name}`);
40
57
  }
41
58
 
42
- spinner.succeed(`Removed ${name}`);
59
+ pkgJson.save();
60
+ lockfile.save();
61
+ this.logger.success(`\nRemoved ${args.length} package(s)`);
43
62
  }
63
+ }
44
64
 
45
- pkgJson.save();
46
- lockfile.save();
47
- logger.success(`\nRemoved ${args.length} package(s)`);
48
- };
65
+ module.exports = UninstallCommand;
@@ -1,63 +1,104 @@
1
1
  'use strict';
2
2
 
3
+ const BaseCommand = require('./base-command');
3
4
  const PackageJSON = require('../core/package-json');
4
5
  const registry = require('../core/registry');
5
6
  const semver = require('../utils/semver');
6
7
  const { Spinner } = require('../utils/progress');
7
- const logger = require('../utils/logger');
8
-
9
- /** jpm update [pkg] | jpm outdated */
10
- module.exports = async function update(args, flags, command) {
11
- const cwd = process.cwd();
12
- const pkgJson = PackageJSON.fromDir(cwd);
13
- const allDeps = pkgJson.allDeps();
14
-
15
- const targets = args.length
16
- ? args.reduce((acc, n) => { if (allDeps[n]) acc[n] = allDeps[n]; return acc; }, {})
17
- : allDeps;
18
-
19
- const spinner = new Spinner(`Checking ${Object.keys(targets).length} packages for updates...`).start();
20
- const rows = [];
21
-
22
- await Promise.all(
23
- Object.entries(targets).map(async ([name, range]) => {
24
- try {
25
- const versions = await registry.getVersions(name);
26
- const latest = await registry.getLatest(name);
27
- const current = semver.maxSatisfying(versions, range);
28
- const wanted = semver.maxSatisfying(versions, range);
29
- const isOutdated = latest && current && semver.gt(latest, current);
30
-
31
- rows.push({
32
- Package: name,
33
- Current: current || 'n/a',
34
- Wanted: wanted || 'n/a',
35
- Latest: latest || 'n/a',
36
- Status: isOutdated
37
- ? logger.c.yellow('outdated')
38
- : logger.c.green('up to date'),
39
- });
40
- } catch {
41
- rows.push({ Package: name, Current: '?', Wanted: '?', Latest: '?', Status: logger.c.red('error') });
8
+
9
+ /**
10
+ * UpdateCommand handles the 'jpm update' and 'jpm outdated' commands.
11
+ * It checks for newer versions of installed packages and can perform updates.
12
+ */
13
+ class UpdateCommand extends BaseCommand {
14
+ constructor() {
15
+ super('update');
16
+ }
17
+
18
+ /**
19
+ * Executes the update or outdated check.
20
+ *
21
+ * @param {string[]} args - Optional package names to limit the check
22
+ * @param {Object} flags - CLI flags
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ const cwd = process.cwd();
27
+ const pkgJson = PackageJSON.fromDir(cwd);
28
+ const allDeps = pkgJson.allDeps();
29
+
30
+ const targets = args.length
31
+ ? args.reduce((acc, n) => { if (allDeps[n]) acc[n] = allDeps[n]; return acc; }, {})
32
+ : allDeps;
33
+
34
+ if (Object.keys(targets).length === 0) {
35
+ this.logger.info('No dependencies found to check.');
36
+ return;
37
+ }
38
+
39
+ const spinner = new Spinner(`Checking ${Object.keys(targets).length} packages for updates...`).start();
40
+ const rows = [];
41
+
42
+ await Promise.all(
43
+ Object.entries(targets).map(async ([name, range]) => {
44
+ try {
45
+ const versions = await registry.getVersions(name);
46
+ const latest = await registry.getLatest(name);
47
+ const current = semver.maxSatisfying(versions, range);
48
+ const wanted = semver.maxSatisfying(versions, range);
49
+ const isOutdated = latest && current && semver.gt(latest, current);
50
+
51
+ rows.push({
52
+ Package: name,
53
+ Current: current || 'n/a',
54
+ Wanted: wanted || 'n/a',
55
+ Latest: latest || 'n/a',
56
+ Status: isOutdated
57
+ ? this.logger.c.yellow('outdated')
58
+ : this.logger.c.green('up to date'),
59
+ });
60
+ } catch (err) {
61
+ rows.push({
62
+ Package: name,
63
+ Current: '?',
64
+ Wanted: '?',
65
+ Latest: '?',
66
+ Status: this.logger.c.red('error')
67
+ });
68
+ }
69
+ })
70
+ );
71
+
72
+ spinner.succeed('Done');
73
+
74
+ // Handle 'outdated' alias/mode
75
+ if (this.name === 'outdated' || flags.outdated) {
76
+ const outdated = rows.filter(r => r.Status.includes('outdated'));
77
+ if (!outdated.length) {
78
+ this.logger.success('All packages are up to date!');
79
+ return;
42
80
  }
43
- })
44
- );
81
+ this.logger.table(outdated, ['Package', 'Current', 'Wanted', 'Latest', 'Status']);
82
+ return;
83
+ }
84
+
85
+ // Filter for packages that actually need updating
86
+ const outdatedPkgs = rows.filter(r => r.Status.includes('outdated'));
87
+ if (!outdatedPkgs.length) {
88
+ this.logger.success('All packages are up to date!');
89
+ return;
90
+ }
91
+
92
+ this.logger.info(`Updating ${outdatedPkgs.length} package(s)...`);
45
93
 
46
- spinner.succeed('Done');
94
+ // Use dynamic import/require to avoid circular dependency with InstallCommand
95
+ const InstallCommand = require('./install');
96
+ const installInstance = new InstallCommand();
47
97
 
48
- if (command === 'outdated') {
49
- const outdated = rows.filter(r => r.Status.includes('outdated'));
50
- if (!outdated.length) { logger.success('All packages are up to date!'); return; }
51
- logger.table(outdated, ['Package', 'Current', 'Wanted', 'Latest', 'Status']);
52
- return;
98
+ const pkgArgs = outdatedPkgs.map(p => `${p.Package}@${p.Latest}`);
99
+ await installInstance.run(pkgArgs, flags);
53
100
  }
101
+ }
54
102
 
55
- // Actually update
56
- const outdatedPkgs = rows.filter(r => r.Status.includes('outdated'));
57
- if (!outdatedPkgs.length) { logger.success('All packages are up to date!'); return; }
103
+ module.exports = UpdateCommand;
58
104
 
59
- logger.info(`Updating ${outdatedPkgs.length} package(s)...`);
60
- const installCmd = require('./install');
61
- const pkgArgs = outdatedPkgs.map(p => `${p.Package}@${p.Latest}`);
62
- await installCmd(pkgArgs, flags);
63
- };