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.
package/bin/jpm.js CHANGED
@@ -238,7 +238,13 @@ async function main() {
238
238
 
239
239
  const handler = loader();
240
240
  try {
241
- await handler(args, flags, command);
241
+ if (typeof handler === 'function' && handler.prototype instanceof (require('../src/commands/base-command'))) {
242
+ const instance = new handler(command);
243
+ await instance.run(args, flags);
244
+ } else {
245
+ // Backward compatibility for functional commands
246
+ await handler(args, flags, command);
247
+ }
242
248
  } catch (err) {
243
249
  if (flags.loglevel === 'debug' || flags.loglevel === 'verbose') {
244
250
  logger.error(err.stack || err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jpm-pkg",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Joint Package Manager — Joint, universal, advanced, and blazing fast package manager for Node.js and Bun.",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -2,55 +2,84 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const BaseCommand = require('./base-command');
5
6
  const auditSec = require('../security/audit');
6
- const PackageJSON = require('../core/package-json');
7
7
  const { Spinner } = require('../utils/progress');
8
- const logger = require('../utils/logger');
9
- const config = require('../utils/config');
10
-
11
- module.exports = async function auditCmd(args, flags) {
12
- const cwd = process.cwd();
13
- const nodeModules = path.join(cwd, 'node_modules');
14
-
15
- // Collect all installed packages from node_modules
16
- const spinner = new Spinner('Scanning installed packages...').start();
17
- const installed = [];
18
-
19
- if (fs.existsSync(nodeModules)) {
20
- for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) {
21
- // Handle scoped packages (@org)
22
- if (entry.name.startsWith('@') && entry.isDirectory()) {
23
- const scopeDir = path.join(nodeModules, entry.name);
24
- for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
25
- const pkgJson = path.join(scopeDir, scoped.name, 'package.json');
26
- if (fs.existsSync(pkgJson)) {
27
- try {
28
- const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'));
29
- installed.push({ name: pkg.name, version: pkg.version });
30
- } catch { }
8
+
9
+ /**
10
+ * AuditCommand handles the 'jpm audit' and 'jpm scan' commands.
11
+ * It performs a security and integrity audit of all installed packages.
12
+ */
13
+ class AuditCommand extends BaseCommand {
14
+ constructor() {
15
+ super('audit');
16
+ }
17
+
18
+ /**
19
+ * Executes the security audit.
20
+ *
21
+ * @param {string[]} args - Optional arguments
22
+ * @param {Object} flags - CLI flags (e.g., --level)
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ const cwd = process.cwd();
27
+ const nodeModules = path.join(cwd, 'node_modules');
28
+
29
+ const spinner = new Spinner('Scanning installed packages...').start();
30
+ const installed = [];
31
+
32
+ if (fs.existsSync(nodeModules)) {
33
+ for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) {
34
+ // Handle scoped packages (@org/pkg)
35
+ if (entry.name.startsWith('@') && entry.isDirectory()) {
36
+ const scopeDir = path.join(nodeModules, entry.name);
37
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
38
+ const pkgJsonPath = path.join(scopeDir, scoped.name, 'package.json');
39
+ this._tryAddPackage(pkgJsonPath, installed);
31
40
  }
32
- }
33
- } else if (entry.isDirectory() && !entry.name.startsWith('.')) {
34
- const pkgJson = path.join(nodeModules, entry.name, 'package.json');
35
- if (fs.existsSync(pkgJson)) {
36
- try {
37
- const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'));
38
- installed.push({ name: pkg.name, version: pkg.version });
39
- } catch { }
41
+ } else if (entry.isDirectory() && !entry.name.startsWith('.')) {
42
+ const pkgJsonPath = path.join(nodeModules, entry.name, 'package.json');
43
+ this._tryAddPackage(pkgJsonPath, installed);
40
44
  }
41
45
  }
42
46
  }
43
- }
44
47
 
45
- spinner.succeed(`Found ${installed.length} installed packages`);
48
+ spinner.succeed(`Found ${installed.length} installed packages`);
49
+
50
+ const level = flags.level || this.config.auditLevel || 'moderate';
51
+ const auditSpinner = new Spinner('Fetching advisory database...').start();
46
52
 
47
- const level = flags.level || config.auditLevel || 'moderate';
48
- const auditSpinner = new Spinner('Fetching advisory database...').start();
53
+ const { vulnerabilities, stats, total, error } = await auditSec.audit(installed, { level });
54
+ auditSpinner.succeed('Audit complete');
49
55
 
50
- const { vulnerabilities, stats, total, error } = await auditSec.audit(installed, { level });
51
- auditSpinner.succeed('Audit complete');
56
+ auditSec.formatAuditResults({ vulnerabilities, stats, total, error });
57
+
58
+ if (total > 0) {
59
+ process.exitCode = 1;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Internal helper to read and add package metadata to the list.
65
+ *
66
+ * @param {string} pkgJsonPath - Path to the package.json file
67
+ * @param {Object[]} list - Reference to the installed packages list
68
+ * @private
69
+ */
70
+ _tryAddPackage(pkgJsonPath, list) {
71
+ if (fs.existsSync(pkgJsonPath)) {
72
+ try {
73
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
74
+ if (pkg.name && pkg.version) {
75
+ list.push({ name: pkg.name, version: pkg.version });
76
+ }
77
+ } catch (err) {
78
+ // Silently skip malformed package.json files
79
+ }
80
+ }
81
+ }
82
+ }
52
83
 
53
- auditSec.formatAuditResults({ vulnerabilities, stats, total, error });
84
+ module.exports = AuditCommand;
54
85
 
55
- if (total > 0) process.exitCode = 1;
56
- };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../utils/logger');
4
+ const config = require('../utils/config');
5
+
6
+ /**
7
+ * BaseCommand is the abstract base class for all JPM CLI commands.
8
+ * It provides common infrastructure for argument parsing, validation, and logging.
9
+ *
10
+ * Each command must implement the `run` method.
11
+ *
12
+ * @abstract
13
+ */
14
+ class BaseCommand {
15
+ /**
16
+ * @param {string} name - The canonical name of the command (e.g., 'install')
17
+ * @param {Object} [options={}] - Command configuration options
18
+ */
19
+ constructor(name, options = {}) {
20
+ /** @type {string} */
21
+ this.name = name;
22
+ /** @type {Object} */
23
+ this.options = options;
24
+ /** @type {Object} */
25
+ this.config = config;
26
+ /** @type {Object} */
27
+ this.logger = logger;
28
+ }
29
+
30
+ /**
31
+ * The main execution entry point for the command.
32
+ * Must be implemented by subclasses.
33
+ *
34
+ * @param {string[]} args - Positional command line arguments
35
+ * @param {Object} flags - Parsed CLI flags/options
36
+ * @returns {Promise<void>}
37
+ * @abstract
38
+ */
39
+ async run(args, flags) {
40
+ throw new Error(`Command "${this.name}" does not implement the run() method.`);
41
+ }
42
+
43
+ /**
44
+ * Displays command-specific help information.
45
+ * Can be overridden by subclasses for more detailed help.
46
+ */
47
+ help() {
48
+ this.logger.log(`Usage: jpm ${this.name} [options] [args]`);
49
+ }
50
+
51
+ /**
52
+ * Validates command arguments. Can be overridden by subclasses.
53
+ *
54
+ * @param {string[]} args - Positional arguments
55
+ * @returns {boolean} True if arguments are valid
56
+ */
57
+ validate(args) {
58
+ return true;
59
+ }
60
+ }
61
+
62
+ module.exports = BaseCommand;
@@ -1,59 +1,102 @@
1
1
  'use strict';
2
2
 
3
- const config = require('../utils/config');
4
- const logger = require('../utils/logger');
5
-
6
- module.exports = async function configCmd(args, flags) {
7
- const [action, key, ...rest] = args;
8
- const value = rest.join(' ');
9
-
10
- switch (action) {
11
- case 'get':
12
- if (!key) { logger.error('Usage: jpm config get <key>'); process.exit(1); }
13
- const val = config.get(key);
14
- if (val === undefined) { logger.warn(`Key "${key}" not set`); }
15
- else { logger.log(String(val)); }
16
- break;
17
-
18
- case 'set':
19
- if (!key || value === '') { logger.error('Usage: jpm config set <key> <value>'); process.exit(1); }
20
- const layer = flags.global ? 'user' : 'project';
21
- config.set(key, value, layer);
22
- logger.success(`Set ${key} = ${value} (${layer})`);
23
- break;
24
-
25
- case 'delete':
26
- case 'del':
27
- if (!key) { logger.error('Usage: jpm config delete <key>'); process.exit(1); }
28
- config.delete(key, flags.global ? 'user' : 'project');
29
- logger.success(`Deleted key: ${key}`);
30
- break;
31
-
32
- case 'list':
33
- case 'ls': {
34
- const all = config.list();
35
- const rows = Object.entries(all).map(([k, v]) => ({ Key: k, Value: String(v) }));
36
- logger.table(rows, ['Key', 'Value']);
37
- break;
3
+ const BaseCommand = require('./base-command');
4
+
5
+ /**
6
+ * ConfigCommand handles the 'jpm config' commands for managing settings.
7
+ * It provides subcommands for getting, setting, deleting, and listing configurations.
8
+ */
9
+ class ConfigCommand extends BaseCommand {
10
+ constructor() {
11
+ super('config');
12
+ }
13
+
14
+ /**
15
+ * Executes the configuration management actions.
16
+ *
17
+ * @param {string[]} args - Action and optional key/value arguments
18
+ * @param {Object} flags - CLI flags (e.g., --global)
19
+ * @returns {Promise<void>}
20
+ */
21
+ async run(args, flags) {
22
+ const [action, key, ...rest] = args;
23
+ const value = rest.join(' ');
24
+
25
+ switch (action) {
26
+ case 'get':
27
+ if (!key) {
28
+ this.logger.error('Usage: jpm config get <key>');
29
+ throw new Error('Key missing for config get.');
30
+ }
31
+ const val = this.config.get(key);
32
+ if (val === undefined) {
33
+ this.logger.warn(`Key "${key}" not set`);
34
+ } else {
35
+ this.logger.log(String(val));
36
+ }
37
+ break;
38
+
39
+ case 'set':
40
+ if (!key || value === '') {
41
+ this.logger.error('Usage: jpm config set <key> <value>');
42
+ throw new Error('Key or value missing for config set.');
43
+ }
44
+ const layer = flags.global ? 'user' : 'project';
45
+ this.config.set(key, value, layer);
46
+ this.logger.success(`Set ${key} = ${value} (${layer})`);
47
+ break;
48
+
49
+ case 'delete':
50
+ case 'del':
51
+ if (!key) {
52
+ this.logger.error('Usage: jpm config delete <key>');
53
+ throw new Error('Key missing for config delete.');
54
+ }
55
+ this.config.delete(key, flags.global ? 'user' : 'project');
56
+ this.logger.success(`Deleted key: ${key}`);
57
+ break;
58
+
59
+ case 'list':
60
+ case 'ls': {
61
+ const allConfigs = this.config.list();
62
+ const rows = Object.entries(allConfigs).map(([k, v]) => ({
63
+ Key: k,
64
+ Value: String(v)
65
+ }));
66
+ this.logger.table(rows, ['Key', 'Value']);
67
+ break;
68
+ }
69
+
70
+ default:
71
+ this._renderHelp();
38
72
  }
73
+ }
39
74
 
40
- default:
41
- logger.log(`
42
- ${logger.c.bold('jpm config')} — manage configuration
75
+ /**
76
+ * Internal helper to render the config-specific help message.
77
+ * @private
78
+ */
79
+ _renderHelp() {
80
+ const c = this.logger.c;
81
+ this.logger.log(`
82
+ ${c.bold('jpm config')} — manage configuration
43
83
 
44
- ${logger.c.cyan('jpm config list')} List all configuration values
45
- ${logger.c.cyan('jpm config get <key>')} Get a specific value
46
- ${logger.c.cyan('jpm config set <key> <val>')} Set a value (project .jpmrc)
47
- ${logger.c.cyan('jpm config delete <key>')} Delete a key
84
+ ${c.cyan('jpm config list')} List all configuration values
85
+ ${c.cyan('jpm config get <key>')} Get a specific value
86
+ ${c.cyan('jpm config set <key> <val>')} Set a value (project .jpmrc)
87
+ ${c.cyan('jpm config delete <key>')} Delete a key
48
88
 
49
- ${logger.c.gray('Add --global to modify ~/.jpmrc instead of .jpmrc')}
50
-
51
- ${logger.c.bold('Common keys:')}
52
- registry ${logger.c.gray('Registry URL (default: https://registry.npmjs.org/)')}
53
- loglevel ${logger.c.gray('Log level: silent|error|warn|info|verbose|debug')}
54
- save-exact ${logger.c.gray('Save exact versions (true/false)')}
55
- audit-level ${logger.c.gray('Minimum audit severity: low|moderate|high|critical')}
56
- cache ${logger.c.gray('Cache directory path')}
89
+ ${c.gray('Add --global to modify ~/.jpmrc instead of .jpmrc')}
90
+
91
+ ${c.bold('Common keys:')}
92
+ registry ${c.gray('Registry URL (default: https://registry.npmjs.org/)')}
93
+ loglevel ${c.gray('Log level: silent|error|warn|info|verbose|debug')}
94
+ save-exact ${c.gray('Save exact versions (true/false)')}
95
+ audit-level ${c.gray('Minimum audit severity: low|moderate|high|critical')}
96
+ cache ${c.gray('Cache directory path')}
57
97
  `);
58
98
  }
59
- };
99
+ }
100
+
101
+ module.exports = ConfigCommand;
102
+
@@ -1,78 +1,125 @@
1
1
  'use strict';
2
2
 
3
+ const BaseCommand = require('./base-command');
3
4
  const registry = require('../core/registry');
4
5
  const semver = require('../utils/semver');
5
6
  const { Spinner } = require('../utils/progress');
6
- const logger = require('../utils/logger');
7
-
8
- module.exports = async function info(args, flags) {
9
- if (!args.length) { logger.error('Usage: jpm info <package> [version]'); process.exit(1); }
10
-
11
- let [pkgSpec] = args;
12
- let name, version;
13
-
14
- const lastAt = pkgSpec.lastIndexOf('@');
15
- if (lastAt > 0) { name = pkgSpec.slice(0, lastAt); version = pkgSpec.slice(lastAt + 1); }
16
- else { name = pkgSpec; version = 'latest'; }
17
-
18
- const spinner = new Spinner(`Fetching info for ${name}...`).start();
19
-
20
- let meta, latest, versions, tags;
21
- try {
22
- [meta, latest, versions, tags] = await Promise.all([
23
- registry.getVersion(name, version),
24
- registry.getLatest(name),
25
- registry.getVersions(name),
26
- registry.getDistTags(name),
27
- ]);
28
- spinner.succeed(`${name}@${meta.version}`);
29
- } catch (err) {
30
- spinner.fail(`Package "${name}" not found: ${err.message}`);
31
- process.exit(1);
32
- }
33
7
 
34
- const c = logger.c;
35
- logger.log('');
36
- logger.log(`${c.bold(meta.name)} ${c.gray(`v${meta.version}`)}`);
37
- if (meta.description) logger.log(meta.description);
38
-
39
- logger.log('');
40
- logger.log(`${c.cyan('latest')} ${latest}`);
41
- logger.log(`${c.cyan('license')} ${meta.license || 'n/a'}`);
42
- logger.log(`${c.cyan('author')} ${typeof meta.author === 'object' ? meta.author.name : (meta.author || 'n/a')}`);
43
- if (meta.homepage) logger.log(`${c.cyan('homepage')} ${meta.homepage}`);
44
- if (meta.repository?.url) logger.log(`${c.cyan('repository')} ${meta.repository.url.replace('git+', '')}`);
45
-
46
- // Keywords
47
- if (meta.keywords?.length) {
48
- logger.log(`${c.cyan('keywords')} ${meta.keywords.slice(0, 10).join(', ')}`);
8
+ /**
9
+ * InfoCommand handles the 'jpm info' and 'jpm view' commands.
10
+ * It fetches and displays detailed metadata about a specific package and version.
11
+ */
12
+ class InfoCommand extends BaseCommand {
13
+ constructor() {
14
+ super('info');
49
15
  }
50
16
 
51
- // Dist-tags
52
- if (Object.keys(tags).length > 1) {
53
- logger.log('');
54
- logger.section('Tags');
55
- for (const [tag, ver] of Object.entries(tags)) {
56
- logger.log(` ${tag.padEnd(12)} ${ver}`);
17
+ /**
18
+ * Executes the package information retrieval.
19
+ *
20
+ * @param {string[]} args - Package specification (e.g., 'pkg@1.2.3' or 'pkg')
21
+ * @param {Object} flags - CLI flags
22
+ * @returns {Promise<void>}
23
+ */
24
+ async run(args, flags) {
25
+ if (!args.length) {
26
+ this.logger.error('Usage: jpm info <package> [version]');
27
+ throw new Error('No package name provided.');
57
28
  }
58
- }
59
29
 
60
- // Dependencies
61
- const deps = meta.dependencies || {};
62
- if (Object.keys(deps).length) {
63
- logger.log('');
64
- logger.section('Dependencies');
65
- for (const [dep, range] of Object.entries(deps)) {
66
- logger.log(` ${c.cyan(dep.padEnd(30))} ${range}`);
30
+ let [pkgSpec] = args;
31
+ let name, version;
32
+
33
+ // Parse name and version from spec (handling scoped packages)
34
+ const lastAt = pkgSpec.lastIndexOf('@');
35
+ if (lastAt > 0 && !(pkgSpec.startsWith('@') && pkgSpec.indexOf('@', 1) === -1)) {
36
+ name = pkgSpec.slice(0, lastAt);
37
+ version = pkgSpec.slice(lastAt + 1);
38
+ } else {
39
+ name = pkgSpec;
40
+ version = 'latest';
67
41
  }
42
+
43
+ const spinner = new Spinner(`Fetching info for ${name}...`).start();
44
+
45
+ try {
46
+ const [meta, latest, versions, tags] = await Promise.all([
47
+ registry.getVersion(name, version),
48
+ registry.getLatest(name),
49
+ registry.getVersions(name),
50
+ registry.getDistTags(name),
51
+ ]);
52
+ spinner.succeed(`${name}@${meta.version}`);
53
+
54
+ const c = this.logger.c;
55
+ this.logger.log('');
56
+ this.logger.log(`${c.bold(meta.name)} ${c.gray(`v${meta.version}`)}`);
57
+ if (meta.description) {
58
+ this.logger.log(meta.description);
59
+ }
60
+
61
+ this.logger.log('');
62
+ this.logger.log(`${c.cyan('latest')} ${latest}`);
63
+ this.logger.log(`${c.cyan('license')} ${meta.license || 'n/a'}`);
64
+ this.logger.log(`${c.cyan('author')} ${this._formatAuthor(meta.author)}`);
65
+ if (meta.homepage) {
66
+ this.logger.log(`${c.cyan('homepage')} ${meta.homepage}`);
67
+ }
68
+ if (meta.repository?.url) {
69
+ this.logger.log(`${c.cyan('repository')} ${meta.repository.url.replace('git+', '')}`);
70
+ }
71
+
72
+ // Render keywords
73
+ if (meta.keywords?.length) {
74
+ this.logger.log(`${c.cyan('keywords')} ${meta.keywords.slice(0, 10).join(', ')}`);
75
+ }
76
+
77
+ // Render distribution tags
78
+ if (Object.keys(tags).length > 1) {
79
+ this.logger.log('');
80
+ this.logger.section('Tags');
81
+ for (const [tag, ver] of Object.entries(tags)) {
82
+ this.logger.log(` ${tag.padEnd(12)} ${ver}`);
83
+ }
84
+ }
85
+
86
+ // Render dependencies
87
+ const deps = meta.dependencies || {};
88
+ if (Object.keys(deps).length) {
89
+ this.logger.log('');
90
+ this.logger.section('Dependencies');
91
+ for (const [dep, range] of Object.entries(deps)) {
92
+ this.logger.log(` ${c.cyan(dep.padEnd(30))} ${range}`);
93
+ }
94
+ }
95
+
96
+ // Render recent versions
97
+ const recentVersions = semver.rsort(versions).slice(0, 10);
98
+ this.logger.log('');
99
+ this.logger.section(`Versions (latest 10 of ${versions.length})`);
100
+ this.logger.log(' ' + recentVersions.join(' '));
101
+
102
+ this.logger.log('');
103
+ this.logger.log(c.gray(`jpm install ${name}@${meta.version}`));
104
+ } catch (err) {
105
+ spinner.fail(`Package "${name}" not found: ${err.message}`);
106
+ throw err;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Formats author information into a readable string.
112
+ *
113
+ * @param {string|Object} author - Author data from package.json
114
+ * @returns {string} Formatted author string
115
+ * @private
116
+ */
117
+ _formatAuthor(author) {
118
+ if (!author) return 'n/a';
119
+ if (typeof author === 'object') return author.name || 'n/a';
120
+ return author;
68
121
  }
122
+ }
69
123
 
70
- // Recent versions
71
- const recentVersions = semver.rsort(versions).slice(0, 10);
72
- logger.log('');
73
- logger.section(`Versions (latest 10 of ${versions.length})`);
74
- logger.log(' ' + recentVersions.join(' '));
124
+ module.exports = InfoCommand;
75
125
 
76
- logger.log('');
77
- logger.log(c.gray(`jpm install ${name}@${meta.version}`));
78
- };