jpm-pkg 1.0.5 → 1.0.7

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
@@ -175,9 +175,13 @@ const COMMANDS = {
175
175
  audit: () => require('../src/commands/audit'),
176
176
  run: () => require('../src/commands/run'),
177
177
  init: () => require('../src/commands/init'),
178
- create: () => require('../src/commands/init'),
179
- show: () => require('../src/commands/info'),
178
+ create: () => require('../src/commands/create'),
179
+ info: () => require('../src/commands/info'),
180
180
  view: () => require('../src/commands/info'),
181
+ doctor: () => require('../src/commands/doctor'),
182
+ why: () => require('../src/commands/why'),
183
+ rebuild: () => require('../src/commands/rebuild'),
184
+ link: () => require('../src/commands/link'),
181
185
  list: () => require('../src/commands/list'),
182
186
  ls: () => require('../src/commands/list'),
183
187
  exec: () => require('../src/commands/x'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jpm-pkg",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",
@@ -28,15 +28,13 @@
28
28
  "license": "MIT",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/whomaderules/jpm.git"
31
+ "url": "git+https://github.com/SulmaneDev/jpm.git"
32
32
  },
33
33
  "bugs": {
34
- "url": "https://github.com/whomaderules/jpm/issues"
35
- },
36
- "homepage": "https://github.com/whomaderules/jpm#readme",
37
- "dependencies": {
38
- "tar": "^6.2.0"
34
+ "url": "https://github.com/SulmaneDev/jpm/issues"
39
35
  },
36
+ "homepage": "https://github.com/SulmaneDev/jpm#readme",
37
+ "dependencies": {},
40
38
  "devDependencies": {
41
39
  "jest": "^29.7.0"
42
40
  },
@@ -19,7 +19,7 @@ class AuditCommand extends BaseCommand {
19
19
  * Executes the security audit.
20
20
  *
21
21
  * @param {string[]} args - Optional arguments
22
- * @param {Object} flags - CLI flags (e.g., --level)
22
+ * @param {Object} flags - CLI flags (e.g., --level, --fix)
23
23
  * @returns {Promise<void>}
24
24
  */
25
25
  async run(args, flags) {
@@ -34,10 +34,12 @@ class AuditCommand extends BaseCommand {
34
34
  // Handle scoped packages (@org/pkg)
35
35
  if (entry.name.startsWith('@') && entry.isDirectory()) {
36
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);
40
- }
37
+ try {
38
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
39
+ const pkgJsonPath = path.join(scopeDir, scoped.name, 'package.json');
40
+ this._tryAddPackage(pkgJsonPath, installed);
41
+ }
42
+ } catch (e) { /* skip */ }
41
43
  } else if (entry.isDirectory() && !entry.name.startsWith('.')) {
42
44
  const pkgJsonPath = path.join(nodeModules, entry.name, 'package.json');
43
45
  this._tryAddPackage(pkgJsonPath, installed);
@@ -55,7 +57,37 @@ class AuditCommand extends BaseCommand {
55
57
 
56
58
  auditSec.formatAuditResults({ vulnerabilities, stats, total, error });
57
59
 
58
- if (total > 0) {
60
+ // Handle --fix
61
+ if (vulnerabilities.length > 0 && flags.fix) {
62
+ const fixable = vulnerabilities.filter(v => v.fixedIn && v.package !== 'undefined');
63
+ if (fixable.length > 0) {
64
+ this.logger.log('');
65
+ this.logger.info(`Attempting to fix ${fixable.length} vulnerabilities...`);
66
+
67
+ const Engine = require('../core/engine');
68
+ const engine = new Engine(cwd);
69
+
70
+ // Deduplicate and prepare package specs
71
+ const seen = new Set();
72
+ const toInstall = [];
73
+ for (const v of fixable) {
74
+ if (seen.has(v.package)) continue;
75
+ seen.add(v.package);
76
+ toInstall.push({ name: v.package, version: v.fixedIn });
77
+ }
78
+
79
+ try {
80
+ await engine.install(toInstall, { flags });
81
+ this.logger.success(`Applied security patches for ${toInstall.length} packages.`);
82
+ } catch (err) {
83
+ this.logger.error(`Failed to apply fixes: ${err.message}`);
84
+ }
85
+ } else {
86
+ this.logger.info('No automated fixes available for these vulnerabilities.');
87
+ }
88
+ }
89
+
90
+ if (total > 0 && !flags.fix) {
59
91
  process.exitCode = 1;
60
92
  }
61
93
  }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const BaseCommand = require('./base-command');
4
+ const ExecCommand = require('./x');
5
+
6
+ /**
7
+ * CreateCommand handles project scaffolding.
8
+ * It's a specialized wrapper around 'jpm x' (exec) that defaults to 'create-' prefixed packages.
9
+ * For example: 'jpm create vite' will execute 'create-vite'.
10
+ */
11
+ class CreateCommand extends BaseCommand {
12
+ constructor() {
13
+ super('create');
14
+ }
15
+
16
+ /**
17
+ * Executes the scaffolding process.
18
+ *
19
+ * @param {string[]} args - Template name and optional generator arguments
20
+ * @param {Object} flags - CLI flags
21
+ * @returns {Promise<void>}
22
+ */
23
+ async run(args, flags) {
24
+ if (!args.length) {
25
+ this.logger.error('Usage: jpm create <template> [options]');
26
+ this.logger.log('Example: jpm create vite');
27
+ return;
28
+ }
29
+
30
+ const template = args[0];
31
+ // Convention: 'create-xxx' packages are used for scaffolding
32
+ const pkgName = template.includes('/') || template.startsWith('create-')
33
+ ? template
34
+ : `create-${template}`;
35
+
36
+ const remainingArgs = args.slice(1);
37
+
38
+ this.logger.info(`Scaffolding project using ${this.logger.c.cyan(pkgName)}...`);
39
+
40
+ /** @type {ExecCommand} */
41
+ const exec = new ExecCommand();
42
+
43
+ // Inherit config and logger from this command
44
+ exec.config = this.config;
45
+ exec.logger = this.logger;
46
+
47
+ try {
48
+ // jpm create vite app-name -- --template react
49
+ // We pass the package name and all following arguments to ExecCommand
50
+ await exec.run([pkgName, ...remainingArgs], flags);
51
+ } catch (err) {
52
+ this.logger.error(`Scaffolding failed: ${err.message}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = CreateCommand;
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const BaseCommand = require('./base-command');
7
+ const http = require('../utils/http');
8
+ const registry = require('../core/registry');
9
+
10
+ /**
11
+ * DoctorCommand handles the 'jpm doctor' command.
12
+ * It performs various health checks on the environment and project.
13
+ */
14
+ class DoctorCommand extends BaseCommand {
15
+ constructor() {
16
+ super('doctor');
17
+ }
18
+
19
+ /**
20
+ * Executes the health checks.
21
+ *
22
+ * @param {string[]} args - CLI arguments (unused)
23
+ * @param {Object} flags - CLI flags
24
+ * @returns {Promise<void>}
25
+ */
26
+ async run(args, flags) {
27
+ this.logger.section('JPM Doctor — Environment Health Check');
28
+
29
+ const c = this.logger.c;
30
+ let issues = 0;
31
+
32
+ // 1. Check Node.js Environment
33
+ const nodeVersion = process.version;
34
+ const platform = process.platform;
35
+ this.logger.log(`${c.cyan('Node.js')} ${nodeVersion} (${platform})`);
36
+
37
+ const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
38
+ if (major < 18) {
39
+ this.logger.warn(` ! JPM recommends Node.js v18 or higher (found ${nodeVersion})`);
40
+ issues++;
41
+ }
42
+
43
+ // 2. Check Registry Connectivity
44
+ const registryUrl = this.config.get('registry');
45
+ this.logger.log(`${c.cyan('Registry')} ${registryUrl}`);
46
+ try {
47
+ const start = Date.now();
48
+ await http.getJSON(`${registryUrl.replace(/\/$/, '')}/ping`);
49
+ const latency = Date.now() - start;
50
+ this.logger.log(` ${c.green('✓')} Connected (${latency}ms)`);
51
+ } catch (err) {
52
+ this.logger.error(` ${c.red('✖')} Could not connect to registry: ${err.message}`);
53
+ issues++;
54
+ }
55
+
56
+ // 3. Check Cache Directory
57
+ const cacheDir = this.config.cacheDir;
58
+ this.logger.log(`${c.cyan('Cache Dir')} ${cacheDir}`);
59
+ try {
60
+ if (!fs.existsSync(cacheDir)) {
61
+ this.logger.log(' - Directory does not exist (will be created on first use)');
62
+ } else {
63
+ fs.accessSync(cacheDir, fs.constants.R_OK | fs.constants.W_OK);
64
+ const stats = fs.statSync(cacheDir);
65
+ this.logger.log(` ${c.green('✓')} Writable (${stats.mode.toString(8)})`);
66
+ }
67
+ } catch (err) {
68
+ this.logger.error(` ${c.red('✖')} Cache directory is not accessible: ${err.message}`);
69
+ issues++;
70
+ }
71
+
72
+ // 4. Check Local Project
73
+ const projectRoot = this.config.get('prefix') || process.cwd();
74
+ const pkgFile = path.join(projectRoot, 'package.json');
75
+ this.logger.log(`${c.cyan('Project')} ${projectRoot}`);
76
+ if (fs.existsSync(pkgFile)) {
77
+ this.logger.log(` ${c.green('✓')} package.json found`);
78
+ } else {
79
+ this.logger.warn(' ! No package.json found in current directory');
80
+ // Not necessarily an issue for a global check, but worth noting
81
+ }
82
+
83
+ this.logger.log('');
84
+ if (issues === 0) {
85
+ this.logger.success('Your environment is healthy! No issues found.');
86
+ } else {
87
+ this.logger.warn(`Found ${issues} potential issue(s). Check the logs above.`);
88
+ }
89
+ }
90
+ }
91
+
92
+ module.exports = DoctorCommand;
@@ -1,10 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const BaseCommand = require('./base-command');
4
- const Resolver = require('../core/resolver');
5
- const Installer = require('../core/installer');
6
- const Lockfile = require('../core/lockfile');
7
- const PackageJSON = require('../core/package-json');
8
4
  const { Spinner } = require('../utils/progress');
9
5
 
10
6
  /**
@@ -25,8 +21,9 @@ class InstallCommand extends BaseCommand {
25
21
  */
26
22
  async run(args, flags) {
27
23
  const cwd = process.cwd();
28
- const pkgJson = PackageJSON.fromDir(cwd);
29
- const lockfile = new Lockfile(cwd);
24
+ const Engine = require('../core/engine');
25
+ const engine = new Engine(cwd);
26
+
30
27
  const isDev = flags.D || flags['save-dev'];
31
28
  const isExact = flags.E || flags['save-exact'] || this.config.saveExact;
32
29
  const noSave = flags['no-save'];
@@ -45,95 +42,34 @@ class InstallCommand extends BaseCommand {
45
42
  toInstall.push({ name, version });
46
43
  }
47
44
  } else {
48
- // General install from package.json
49
- const spinner = new Spinner('Reading package.json...').start();
45
+ // General install from package.json/lockfile
46
+ if (engine.lockfile.exists()) {
47
+ await engine.installFromLock(flags);
48
+ return;
49
+ }
50
+
50
51
  const allDeps = {
51
- ...pkgJson.dependencies,
52
- ...(flags.production ? {} : pkgJson.devDependencies),
52
+ ...engine.pkgJson.dependencies,
53
+ ...(flags.production ? {} : engine.pkgJson.devDependencies),
53
54
  };
54
- spinner.succeed(`Found ${Object.keys(allDeps).length} dependencies`);
55
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`);
56
+ if (!Object.keys(allDeps).length) {
57
+ this.logger.info('Nothing to install.');
66
58
  return;
67
59
  }
68
60
 
69
61
  toInstall = Object.entries(allDeps).map(([name, version]) => ({ name, version }));
70
62
  }
71
63
 
72
- if (!toInstall.length) {
73
- this.logger.info('Nothing to install.');
74
- return;
75
- }
76
-
77
- // ── Resolve ────────────────────────────────────────────────────────────────
78
- const resolveSpinner = new Spinner(`Resolving ${toInstall.length} package(s)...`).start();
79
- const resolver = new Resolver();
80
-
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
- }
87
-
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
- }
98
-
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
- }
104
-
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
- }
114
-
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
- }
125
-
126
- if (!dryRun) {
127
- lockfile.update(resolvedMap).save();
128
- this.logger.verbose('Updated jpm-lock.json');
129
- }
64
+ // ── Core Installation Flow ────────────────────────────────────────────────
65
+ const resolvedMap = await engine.install(toInstall, {
66
+ dev: !!isDev,
67
+ exact: !!isExact,
68
+ noSave: !!noSave,
69
+ flags
70
+ });
130
71
 
131
72
  // ── Summary ────────────────────────────────────────────────────────────────
132
- const dupes = resolver.deduplicate();
133
- if (dupes.length) {
134
- this.logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
135
- }
136
-
137
73
  const directly = toInstall.map(({ name }) => {
138
74
  const r = [...resolvedMap.values()].find(m => m.name === name);
139
75
  return r ? `${r.name}@${r.version}` : name;
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const BaseCommand = require('./base-command');
7
+ const { mkdirp, symlink } = require('../utils/fs');
8
+ const PackageJSON = require('../core/package-json');
9
+
10
+ /**
11
+ * LinkCommand handles 'jpm link' and 'jpm link <package>'.
12
+ * Facilitates local package development by creating symbolic links between projects.
13
+ * Similar to 'npm link'.
14
+ */
15
+ class LinkCommand extends BaseCommand {
16
+ constructor() {
17
+ super('link');
18
+ /** @type {string} */
19
+ this.globalLinkDir = path.join(os.homedir(), '.jpm', 'links');
20
+ }
21
+
22
+ /**
23
+ * Executes the linking process.
24
+ *
25
+ * @param {string[]} args - Optional package name to link into current project
26
+ * @param {Object} flags - CLI flags
27
+ * @returns {Promise<void>}
28
+ */
29
+ async run(args, flags) {
30
+ const cwd = process.cwd();
31
+
32
+ if (args.length === 0) {
33
+ // Step 1: Run 'jpm link' in the package directory to register it globally
34
+ await this._linkGlobal(cwd);
35
+ } else {
36
+ // Step 2: Run 'jpm link <pkg>' in the consumer project to use the linked package
37
+ for (const pkgName of args) {
38
+ await this._linkToProject(pkgName, cwd);
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Registers a local package directory in the global JPM links folder.
45
+ *
46
+ * @param {string} pkgDir - Absolute path to the package directory
47
+ * @returns {Promise<void>}
48
+ * @private
49
+ */
50
+ async _linkGlobal(pkgDir) {
51
+ let pkg;
52
+ try {
53
+ pkg = PackageJSON.fromDir(pkgDir);
54
+ } catch (err) {
55
+ this.logger.error(`Could not read package.json in ${pkgDir}`);
56
+ return;
57
+ }
58
+
59
+ mkdirp(this.globalLinkDir);
60
+ const dest = path.join(this.globalLinkDir, pkg.name);
61
+
62
+ this.logger.info(`Registering ${this.logger.c.cyan(pkg.name)} globally...`);
63
+
64
+ try {
65
+ symlink(pkgDir, dest);
66
+ this.logger.success(`${this.logger.c.bold(pkg.name)} linked globally to ${pkgDir}`);
67
+ this.logger.log(`Run ${this.logger.c.gray(`jpm link ${pkg.name}`)} in another project to use it.`);
68
+ } catch (err) {
69
+ this.logger.error(`Failed to create global link: ${err.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Links a globally registered package into a project's node_modules.
75
+ *
76
+ * @param {string} pkgName - Name of the package to link
77
+ * @param {string} projectDir - Absolute path to the consumer project
78
+ * @returns {Promise<void>}
79
+ * @private
80
+ */
81
+ async _linkToProject(pkgName, projectDir) {
82
+ const globalSrc = path.join(this.globalLinkDir, pkgName);
83
+ if (!fs.existsSync(globalSrc)) {
84
+ this.logger.error(`Package "${pkgName}" is not linked globally.`);
85
+ this.logger.log(`Run ${this.logger.c.gray('jpm link')} inside the ${pkgName} directory first.`);
86
+ return;
87
+ }
88
+
89
+ const projectNM = path.join(projectDir, 'node_modules', pkgName);
90
+ this.logger.info(`Linking ${this.logger.c.cyan(pkgName)} into project...`);
91
+
92
+ try {
93
+ symlink(globalSrc, projectNM);
94
+ this.logger.success(`${this.logger.c.bold(pkgName)} linked to ${this.logger.c.gray(projectNM)}`);
95
+ } catch (err) {
96
+ this.logger.error(`Failed to link ${pkgName}: ${err.message}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ module.exports = LinkCommand;
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+ const BaseCommand = require('./base-command');
7
+ const { Spinner } = require('../utils/progress');
8
+
9
+ /**
10
+ * RebuildCommand handles the 'jpm rebuild' command.
11
+ * It re-runs lifecycle scripts (preinstall, install, postinstall) for installed packages.
12
+ */
13
+ class RebuildCommand extends BaseCommand {
14
+ constructor() {
15
+ super('rebuild');
16
+ }
17
+
18
+ /**
19
+ * Executes the rebuild process across node_modules.
20
+ *
21
+ * @param {string[]} args - Optional package names to limit the rebuild to
22
+ * @param {Object} flags - CLI flags
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ const cwd = process.cwd();
27
+ const nmDir = path.join(cwd, 'node_modules');
28
+
29
+ if (!fs.existsSync(nmDir)) {
30
+ this.logger.error('node_modules directory not found. Run jpm install first.');
31
+ return;
32
+ }
33
+
34
+ const targets = args.length ? new Set(args) : null;
35
+ const scanSpinner = new Spinner('Scanning node_modules for scripts...').start();
36
+
37
+ const pkgsWithScripts = [];
38
+ const entries = fs.readdirSync(nmDir, { withFileTypes: true });
39
+
40
+ for (const entry of entries) {
41
+ // Skip non-package entries (like .bin)
42
+ if (entry.name.startsWith('.')) continue;
43
+
44
+ if (entry.name.startsWith('@')) {
45
+ // Handle scoped packages
46
+ const scopeDir = path.join(nmDir, entry.name);
47
+ try {
48
+ const scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
49
+ for (const scopedEntry of scopedEntries) {
50
+ this._checkPkg(path.join(scopeDir, scopedEntry.name), pkgsWithScripts, targets);
51
+ }
52
+ } catch (e) { /* ignore read errors */ }
53
+ } else {
54
+ this._checkPkg(path.join(nmDir, entry.name), pkgsWithScripts, targets);
55
+ }
56
+ }
57
+
58
+ scanSpinner.succeed(`Found ${pkgsWithScripts.length} package(s) with lifecycle scripts.`);
59
+
60
+ if (pkgsWithScripts.length === 0) {
61
+ this.logger.info('No lifecycle scripts found to run.');
62
+ return;
63
+ }
64
+
65
+ for (const { name, dir, scripts } of pkgsWithScripts) {
66
+ // Lifecycle scripts run in specific order: preinstall -> install -> postinstall
67
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
68
+ if (scripts[hook]) {
69
+ this.logger.section(`▶ ${this.logger.c.cyan(name)} — ${hook}`);
70
+ const result = spawnSync(scripts[hook], {
71
+ cwd: dir,
72
+ shell: true,
73
+ stdio: 'inherit',
74
+ env: { ...process.env, PATH: `${path.join(nmDir, '.bin')}${path.delimiter}${process.env.PATH}` }
75
+ });
76
+
77
+ if (result.status !== 0) {
78
+ this.logger.warn(`Script "${hook}" in ${name} failed with exit code ${result.status}`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ this.logger.success('Rebuild complete!');
85
+ }
86
+
87
+ /**
88
+ * Checks a directory for a package.json containing lifecycle scripts.
89
+ *
90
+ * @param {string} dir - Directory to check
91
+ * @param {Array} list - Array to push matching package info into
92
+ * @param {Set|null} targets - Optional set of package names to filter by
93
+ * @private
94
+ */
95
+ _checkPkg(dir, list, targets) {
96
+ const pkgFile = path.join(dir, 'package.json');
97
+ if (!fs.existsSync(pkgFile)) return;
98
+
99
+ try {
100
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
101
+ if (targets && !targets.has(pkg.name)) return;
102
+
103
+ const scripts = pkg.scripts || {};
104
+ const hasScripts = scripts.install || scripts.preinstall || scripts.postinstall;
105
+
106
+ if (hasScripts) {
107
+ list.push({ name: pkg.name, dir, scripts });
108
+ }
109
+ } catch (err) {
110
+ // skip invalid or unreadable package.json
111
+ }
112
+ }
113
+ }
114
+
115
+ module.exports = RebuildCommand;
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const BaseCommand = require('./base-command');
4
- const PackageJSON = require('../core/package-json');
5
4
  const registry = require('../core/registry');
6
5
  const semver = require('../utils/semver');
7
6
  const { Spinner } = require('../utils/progress');
@@ -24,8 +23,10 @@ class UpdateCommand extends BaseCommand {
24
23
  */
25
24
  async run(args, flags) {
26
25
  const cwd = process.cwd();
27
- const pkgJson = PackageJSON.fromDir(cwd);
28
- const allDeps = pkgJson.allDeps();
26
+ const Engine = require('../core/engine');
27
+ const engine = new Engine(cwd);
28
+
29
+ const allDeps = engine.pkgJson.allDeps();
29
30
 
30
31
  const targets = args.length
31
32
  ? args.reduce((acc, n) => { if (allDeps[n]) acc[n] = allDeps[n]; return acc; }, {})
@@ -91,12 +92,12 @@ class UpdateCommand extends BaseCommand {
91
92
 
92
93
  this.logger.info(`Updating ${outdatedPkgs.length} package(s)...`);
93
94
 
94
- // Use dynamic import/require to avoid circular dependency with InstallCommand
95
- const InstallCommand = require('./install');
96
- const installInstance = new InstallCommand();
95
+ const pkgArgs = outdatedPkgs.map(p => ({
96
+ name: p.Package,
97
+ version: p.Latest
98
+ }));
97
99
 
98
- const pkgArgs = outdatedPkgs.map(p => `${p.Package}@${p.Latest}`);
99
- await installInstance.run(pkgArgs, flags);
100
+ await engine.install(pkgArgs, { flags });
100
101
  }
101
102
  }
102
103
 
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const BaseCommand = require('./base-command');
4
+ const Lockfile = require('../core/lockfile');
5
+ const PackageJSON = require('../core/package-json');
6
+
7
+ /**
8
+ * WhyCommand handles the 'jpm why <pkg>' command.
9
+ * It traces dependency paths to explain why a package is installed.
10
+ */
11
+ class WhyCommand extends BaseCommand {
12
+ constructor() {
13
+ super('why');
14
+ }
15
+
16
+ /**
17
+ * Executes the dependency tracing.
18
+ *
19
+ * @param {string[]} args - Package name to trace
20
+ * @param {Object} flags - CLI flags
21
+ * @returns {Promise<void>}
22
+ */
23
+ async run(args, flags) {
24
+ if (!args.length) {
25
+ this.logger.error('Usage: jpm why <package>');
26
+ return;
27
+ }
28
+
29
+ const target = args[0];
30
+ const cwd = process.cwd();
31
+ const lockfile = new Lockfile(cwd);
32
+ const pkgJson = PackageJSON.fromDir(cwd);
33
+
34
+ if (!lockfile.exists()) {
35
+ this.logger.error('No lockfile found. Run jpm install first.');
36
+ return;
37
+ }
38
+
39
+ const lockData = lockfile.allPackages();
40
+ // Index by name for easier lookup
41
+ const pkgMap = new Map();
42
+ for (const p of lockData) {
43
+ pkgMap.set(p.name, p);
44
+ }
45
+
46
+ const topLevel = pkgJson.allDeps();
47
+ const paths = [];
48
+ const seen = new Set();
49
+
50
+ /**
51
+ * Recursively searches for the target package in the dependency tree.
52
+ *
53
+ * @param {string} currentName - Current package being inspected
54
+ * @param {string[]} currentPath - Breadcrumb path to the current package
55
+ */
56
+ const findPaths = (currentName, currentPath = []) => {
57
+ const pathKey = [...currentPath, currentName].join('>');
58
+ if (seen.has(pathKey)) return;
59
+ seen.add(pathKey);
60
+
61
+ if (currentName === target) {
62
+ paths.push([...currentPath, target]);
63
+ return;
64
+ }
65
+
66
+ const pkg = pkgMap.get(currentName);
67
+ if (!pkg || !pkg.dependencies) return;
68
+
69
+ for (const dep of Object.keys(pkg.dependencies)) {
70
+ // Generic cycle protection: don't revisit same package in current path
71
+ if (currentPath.includes(dep)) continue;
72
+ findPaths(dep, [...currentPath, currentName]);
73
+ }
74
+ };
75
+
76
+ for (const root of Object.keys(topLevel)) {
77
+ findPaths(root, ['(project)']);
78
+ }
79
+
80
+ if (paths.length === 0) {
81
+ this.logger.info(`Package "${this.logger.c.bold(target)}" is not required by any installed package.`);
82
+ return;
83
+ }
84
+
85
+ const c = this.logger.c;
86
+ this.logger.section(`Found ${paths.length} path(s) to ${c.bold(target)}:`);
87
+
88
+ for (const path of paths) {
89
+ const formatted = path.map((p, i) => {
90
+ if (p === '(project)') return c.gray(p);
91
+ if (p === target) return c.yellow(c.bold(p));
92
+ return c.cyan(p);
93
+ }).join(c.gray(' → '));
94
+
95
+ this.logger.log(` ${formatted}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ module.exports = WhyCommand;
package/src/commands/x.js CHANGED
@@ -101,20 +101,37 @@ class ExecCommand extends BaseCommand {
101
101
 
102
102
  // Cleanup temp directory on success unless debugging
103
103
  if (result.status === 0 && this.config.loglevel !== 'debug') {
104
- rimraf(tmpDir);
104
+ this._cleanupTemp(tmpDir);
105
105
  }
106
106
 
107
107
  process.exit(result.status ?? 0);
108
108
 
109
109
  } catch (err) {
110
110
  if (tmpDir && fs.existsSync(tmpDir) && this.config.loglevel !== 'debug') {
111
- rimraf(tmpDir);
111
+ this._cleanupTemp(tmpDir);
112
112
  }
113
113
  resolveSpinner.fail(`Error: ${err.message}`);
114
114
  throw err;
115
115
  }
116
116
  }
117
117
 
118
+ /**
119
+ * Safely cleans up a temporary directory, handling Windows junctions and symlinks.
120
+ *
121
+ * @param {string} tmpDir - Path to the temporary directory
122
+ * @private
123
+ */
124
+ _cleanupTemp(tmpDir) {
125
+ try {
126
+ // On Windows, junctions inside node_modules can sometimes block rimraf
127
+ // if not handled carefully. Our internal rimraf handles recursive delete,
128
+ // but we add an extra safety check here.
129
+ rimraf(tmpDir);
130
+ } catch (err) {
131
+ this.logger.debug(`Minor: Failed to cleanup temp directory ${tmpDir}: ${err.message}`);
132
+ }
133
+ }
134
+
118
135
  /**
119
136
  * Identifies the primary binary name from package metadata.
120
137
  *
package/src/core/cache.js CHANGED
@@ -54,6 +54,28 @@ class Cache {
54
54
  return path.join(this.cacheRoot, safeName, `${version}.json`);
55
55
  }
56
56
 
57
+ /**
58
+ * Initializes a SQLite database for metadata caching if running in Bun.
59
+ *
60
+ * @returns {Object|null} The SQLite database instance or null
61
+ * @private
62
+ */
63
+ _getSQLite() {
64
+ if (typeof Bun === 'undefined') return null;
65
+ if (this._db) return this._db;
66
+
67
+ try {
68
+ const { Database } = require('bun:sqlite');
69
+ const dbPath = path.join(this.cacheRoot, 'cache.sqlite');
70
+ mkdirp(this.cacheRoot);
71
+ this._db = new Database(dbPath);
72
+ this._db.run('CREATE TABLE IF NOT EXISTS metadata (id TEXT PRIMARY KEY, data TEXT)');
73
+ return this._db;
74
+ } catch (e) {
75
+ return null;
76
+ }
77
+ }
78
+
57
79
  /**
58
80
  * Retrieves a package tarball from the cache if it exists.
59
81
  *
@@ -94,6 +116,13 @@ class Cache {
94
116
  * @returns {Promise<void>}
95
117
  */
96
118
  async setMeta(name, version, meta) {
119
+ const db = this._getSQLite();
120
+ if (db) {
121
+ try {
122
+ db.run('INSERT OR REPLACE INTO metadata (id, data) VALUES (?, ?)', [`${name}@${version}`, JSON.stringify(meta)]);
123
+ } catch (e) { /* fallback */ }
124
+ }
125
+
97
126
  const p = this._metaPath(name, version);
98
127
  mkdirp(path.dirname(p));
99
128
  fs.writeFileSync(p, JSON.stringify(meta, null, 2), 'utf8');
@@ -107,6 +136,14 @@ class Cache {
107
136
  * @returns {Promise<Object|null>} Decoded metadata object, or null if not found/invalid
108
137
  */
109
138
  async getMeta(name, version) {
139
+ const db = this._getSQLite();
140
+ if (db) {
141
+ try {
142
+ const row = db.query('SELECT data FROM metadata WHERE id = ?').get(`${name}@${version}`);
143
+ if (row) return JSON.parse(row.data);
144
+ } catch (e) { /* fallback */ }
145
+ }
146
+
110
147
  const p = this._metaPath(name, version);
111
148
  try {
112
149
  return JSON.parse(fs.readFileSync(p, 'utf8'));
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const Resolver = require('./resolver');
5
+ const Installer = require('./installer');
6
+ const Lockfile = require('./lockfile');
7
+ const PackageJSON = require('./package-json');
8
+ const logger = require('../utils/logger');
9
+ const { Spinner } = require('../utils/progress');
10
+
11
+ /**
12
+ * Engine class coordinates the higher-level package management operations.
13
+ * It ties together resolution, installation, and persistence logic.
14
+ */
15
+ class Engine {
16
+ /**
17
+ * @param {string} projectRoot - The absolute path to the project root directory
18
+ */
19
+ constructor(projectRoot) {
20
+ /** @type {string} */
21
+ this.projectRoot = projectRoot;
22
+ /** @type {PackageJSON} */
23
+ this.pkgJson = PackageJSON.fromDir(projectRoot);
24
+ /** @type {Lockfile} */
25
+ this.lockfile = new Lockfile(projectRoot);
26
+ /** @type {Installer} */
27
+ this.installer = new Installer(projectRoot);
28
+ }
29
+
30
+ /**
31
+ * Executes a full installation flow for a set of packages.
32
+ *
33
+ * @param {Array<{name: string, version: string}>} packages - Packages to install
34
+ * @param {Object} options - Installation options
35
+ * @param {boolean} [options.dev=false] - Whether to install as devDependencies
36
+ * @param {boolean} [options.exact=false] - Whether to save as exact versions
37
+ * @param {boolean} [options.noSave=false] - Whether to skip updating package.json
38
+ * @param {Object} [options.flags={}] - CLI flags
39
+ * @returns {Promise<Map<string, Object>>} The resolved package map
40
+ */
41
+ async install(packages, options = {}) {
42
+ const { dev = false, exact = false, noSave = false, flags = {} } = options;
43
+
44
+ // 1. Resolve
45
+ const resolveSpinner = new Spinner(`Resolving ${packages.length} package(s)...`).start();
46
+ const resolver = new Resolver();
47
+
48
+ const deps = {};
49
+ const devDeps = {};
50
+ for (const { name, version } of packages) {
51
+ if (dev) devDeps[name] = version;
52
+ else deps[name] = version;
53
+ }
54
+
55
+ let resolvedMap;
56
+ try {
57
+ resolvedMap = await resolver.resolve(deps, devDeps, {}, (count) => {
58
+ resolveSpinner.text = `Resolving packages... (${count} resolved)`;
59
+ });
60
+ resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages`);
61
+ } catch (err) {
62
+ resolveSpinner.fail(`Resolution failed: ${err.message}`);
63
+ throw err;
64
+ }
65
+
66
+ // 2. Audit for cycles
67
+ const circular = resolver.findCircular();
68
+ if (circular.length) {
69
+ logger.warn(`Circular dependencies detected:\n ${circular.join('\n ')}`);
70
+ }
71
+
72
+ // 3. Install
73
+ logger.info(`Installing ${resolvedMap.size} packages...`);
74
+ try {
75
+ await this.installer.installAll(resolvedMap, { dryRun: flags['dry-run'], flags });
76
+ } catch (err) {
77
+ logger.error(`Install failed: ${err.message}`);
78
+ throw err;
79
+ }
80
+
81
+ // 4. Persist Changes
82
+ if (!noSave && !flags['dry-run'] && packages.length) {
83
+ for (const { name } of packages) {
84
+ const resolved = [...resolvedMap.values()].find(m => m.name === name);
85
+ if (!resolved) continue;
86
+ this.pkgJson.addDependency(name, resolved.version, { dev, exact });
87
+ }
88
+ this.pkgJson.save();
89
+ logger.verbose('Updated package.json');
90
+ }
91
+
92
+ if (!flags['dry-run']) {
93
+ this.lockfile.update(resolvedMap).save();
94
+ logger.verbose('Updated jpm-lock.json');
95
+ }
96
+
97
+ return resolvedMap;
98
+ }
99
+
100
+ /**
101
+ * Performs a deterministic install from the lockfile.
102
+ *
103
+ * @param {Object} [flags={}] - CLI flags
104
+ * @returns {Promise<void>}
105
+ */
106
+ async installFromLock(flags = {}) {
107
+ if (!this.lockfile.exists()) {
108
+ throw new Error('No lockfile found. Run jpm install first.');
109
+ }
110
+
111
+ logger.info('Using lock file for deterministic install');
112
+ const lockData = this.lockfile.allPackages();
113
+ const spinner = new Spinner('Installing from lock file...').start();
114
+
115
+ const fakeMap = new Map(lockData.map(p => [`${p.name}@${p.version}`, p]));
116
+ await this.installer.installAll(fakeMap, { dryRun: flags['dry-run'], flags });
117
+
118
+ spinner.succeed(`Installed ${lockData.length} packages`);
119
+ }
120
+ }
121
+
122
+ module.exports = Engine;
@@ -3,7 +3,6 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const crypto = require('node:crypto');
6
- const tar = require('tar');
7
6
  const registry = require('./registry');
8
7
  const cache = require('./cache');
9
8
  const { mkdirp, rimraf, tempDir, symlink } = require('../utils/fs');
@@ -170,28 +169,56 @@ class Installer {
170
169
  }
171
170
 
172
171
  /**
173
- * Extracts a tarball to the destination directory.
172
+ * Extracts a tarball's contents into a target directory.
173
+ * Prioritizes system 'tar' for speed and to minimize dependency overhead.
174
174
  *
175
- * @param {string} tgzPath - Path to the tarball file
175
+ * @param {string} tgzPath - Absolute path to the source .tgz file
176
176
  * @param {string} destDir - Target directory for extraction
177
- * @param {string} name - Package name for logging
178
- * @param {string} version - Package version for logging
177
+ * @param {string} name - Package name for error context
178
+ * @param {string} version - Package version for error context
179
+ * @returns {Promise<void>}
179
180
  * @protected
180
181
  */
181
182
  async _extract(tgzPath, destDir, name, version) {
182
183
  rimraf(destDir);
183
184
  mkdirp(destDir);
184
185
 
185
- const absoluteDest = path.resolve(destDir);
186
+ try {
187
+ // Attempt to use system 'tar' (available on Linux, macOS, and Win 10+)
188
+ const { spawnSync } = require('node:child_process');
189
+ const result = spawnSync('tar', [
190
+ '-xzf', tgzPath,
191
+ '-C', destDir,
192
+ '--strip-components=1'
193
+ ], { shell: true, stdio: 'pipe' });
194
+
195
+ if (result.status === 0) {
196
+ logger.verbose(`extracted ${name}@${version} via system tar`);
197
+ return;
198
+ }
199
+ logger.debug(`System tar failed (code ${result.status}): ${result.stderr.toString()}`);
200
+ } catch (err) {
201
+ logger.debug(`System tar not found or failed: ${err.message}`);
202
+ }
186
203
 
204
+ // Fallback to JS-based 'tar' package
205
+ logger.verbose(`falling back to 'tar' package for ${name}@${version}`);
206
+ let tar;
207
+ try {
208
+ tar = require('tar');
209
+ } catch (err) {
210
+ throw new Error(`Extraction failed: system 'tar' not available and 'tar' package not installed. ${err.message}`);
211
+ }
212
+
213
+ const absoluteDest = path.resolve(destDir);
187
214
  await tar.extract({
188
215
  file: tgzPath,
189
216
  cwd: destDir,
190
217
  strip: 1,
191
- filter: (p, stat) => {
218
+ filter: (p) => {
192
219
  const fullPath = path.resolve(destDir, p);
193
220
  if (!fullPath.startsWith(absoluteDest)) {
194
- logger.error(`Zip Slip security violation blocked: ${p} in ${name}@${version}`);
221
+ logger.error(`Zip Slip security violation blocked in ${name}@${version}: ${p}`);
195
222
  return false;
196
223
  }
197
224
  return true;
@@ -260,6 +287,7 @@ class Installer {
260
287
  * Creates symbolic links for binary executables defined in package metadata.
261
288
  *
262
289
  * @param {Object[]} packages - Array of resolved package metadata
290
+ * @returns {Promise<void>}
263
291
  * @protected
264
292
  */
265
293
  async _linkBins(packages) {
@@ -285,7 +313,9 @@ class Installer {
285
313
  try {
286
314
  symlink(src, dest);
287
315
  fs.chmodSync(src, 0o755);
288
- } catch { }
316
+ } catch (err) {
317
+ logger.warn(`Failed to link binary "${binName}" for ${installName}: ${err.message}`);
318
+ }
289
319
  }
290
320
  }
291
321
  }
@@ -149,6 +149,7 @@ class Registry {
149
149
  *
150
150
  * @param {Object.<string, string[]>} requires - Map of package names to lists of required versions
151
151
  * @returns {Promise<Object>} Audit report containing vulnerabilities, advisories, and metadata
152
+ * @throws {Error} If the registry request fails or returns an invalid response
152
153
  */
153
154
  async fetchAdvisories(requires) {
154
155
  const url = `https://registry.npmjs.org/-/npm/v1/security/audits/quick`;
@@ -167,22 +168,29 @@ class Registry {
167
168
  dependencies: packages,
168
169
  });
169
170
 
170
- const res = await request(url, {
171
- method: 'POST',
172
- headers: {
173
- 'Content-Type': 'application/json',
174
- 'Content-Length': Buffer.byteLength(body),
175
- },
176
- body,
177
- timeout: 30_000,
178
- retries: 2,
179
- strict: true, // Security: Always use HTTPS for audits
180
- });
181
-
182
171
  try {
172
+ const res = await request(url, {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Content-Type': 'application/json',
176
+ 'Content-Length': Buffer.byteLength(body),
177
+ },
178
+ body,
179
+ timeout: 30_000,
180
+ retries: 2,
181
+ strict: true, // Security: Always use HTTPS for audits
182
+ });
183
+
184
+ if (res.status < 200 || res.status >= 300) {
185
+ throw new Error(`Registry audit failed with status ${res.status}: ${res.body.slice(0, 200)}`);
186
+ }
187
+
183
188
  return JSON.parse(res.body);
184
- } catch {
185
- return { advisories: {}, metadata: {} };
189
+ } catch (err) {
190
+ logger.error(`Security audit request failed: ${err.message}`);
191
+ // Return empty structure to allow the process to continue if non-critical,
192
+ // but log the error clearly.
193
+ return { advisories: {}, metadata: {}, error: err.message };
186
194
  }
187
195
  }
188
196
  }
package/src/utils/http.js CHANGED
@@ -38,10 +38,39 @@ function checkBreaker() {
38
38
  }
39
39
 
40
40
  /**
41
- * options: { method, headers, timeout, retries, retryDelay, stream }
42
- * Returns: { status, headers, body } or a raw IncomingMessage if stream:true
41
+ * options: { method, headers, timeout, retries, retryDelay, stream, body, strict }
42
+ * Returns: { status, headers, body } or a raw IncomingMessage if stream:true.
43
+ * Uses Bun.fetch if running in a Bun environment for optimized performance.
44
+ *
45
+ * @param {string} url - Target URL
46
+ * @param {Object} [options={}] - Request options
47
+ * @returns {Promise<Object|import('node:http').IncomingMessage>}
43
48
  */
44
- function request(url, options = {}) {
49
+ async function request(url, options = {}) {
50
+ // ── Native Bun Optimization ──────────────────────────────────────────────
51
+ if (typeof Bun !== 'undefined' && !options.stream) {
52
+ try {
53
+ const res = await Bun.fetch(url, {
54
+ method: options.method || 'GET',
55
+ headers: {
56
+ 'User-Agent': USER_AGENT,
57
+ 'Accept-Encoding': 'gzip, deflate',
58
+ 'Accept': 'application/json',
59
+ ...(options.headers || {}),
60
+ },
61
+ body: options.body,
62
+ redirect: 'follow',
63
+ });
64
+ return {
65
+ status: res.status,
66
+ headers: Object.fromEntries(res.headers.entries()),
67
+ body: await res.text(),
68
+ };
69
+ } catch (err) {
70
+ logger.debug(`Bun.fetch failed, falling back to node:http: ${err.message}`);
71
+ }
72
+ }
73
+
45
74
  const {
46
75
  method = 'GET',
47
76
  headers = {},
@@ -78,6 +107,12 @@ function request(url, options = {}) {
78
107
  },
79
108
  };
80
109
 
110
+ /**
111
+ * Executes a single request attempt with retries and circuit breaker logic.
112
+ *
113
+ * @param {number} attemptsLeft - Remaining retry attempts
114
+ * @returns {Promise<Object|import('node:http').IncomingMessage>}
115
+ */
81
116
  function attempt(attemptsLeft) {
82
117
  return new Promise((resolve, reject) => {
83
118
  const req = lib.request(reqOptions, (res) => {
@@ -158,7 +193,9 @@ function request(url, options = {}) {
158
193
  * @returns {Promise<Object>} Parsed JSON object
159
194
  */
160
195
  async function getJSON(url, opts = {}) {
161
- const { status, body } = await request(url, { ...opts, headers: { Accept: 'application/json', ...(opts.headers || {}) } });
196
+ const res = await request(url, { ...opts, headers: { Accept: 'application/json', ...(opts.headers || {}) } });
197
+ const { status, body } = res;
198
+
162
199
  if (status < 200 || status >= 300) {
163
200
  const err = new Error(`HTTP ${status}: ${url}`);
164
201
  err.status = status;
@@ -166,7 +203,7 @@ async function getJSON(url, opts = {}) {
166
203
  throw err;
167
204
  }
168
205
  try {
169
- return JSON.parse(body);
206
+ return typeof body === 'object' ? body : JSON.parse(body);
170
207
  } catch (e) {
171
208
  throw new Error(`Invalid JSON from ${url}: ${e.message}\nBody snippet: ${body.slice(0, 200)}`);
172
209
  }
@@ -206,27 +243,4 @@ async function download(url, destStream, opts = {}) {
206
243
  });
207
244
  }
208
245
 
209
- /**
210
- * Retrieves a list of all packages currently stored in the local cache.
211
- *
212
- * @returns {{ name: string, version: string }[]}
213
- */
214
- function list() {
215
- const root = cacheRoot();
216
- if (!fs.existsSync(root)) return [];
217
- const result = [];
218
- for (const scopeOrName of fs.readdirSync(root)) {
219
- const dir = path.join(root, scopeOrName);
220
- if (!fs.statSync(dir).isDirectory()) continue;
221
- for (const file of fs.readdirSync(dir)) {
222
- if (file.endsWith('.tgz')) {
223
- const version = file.replace('.tgz', '');
224
- const name = scopeOrName.replace('__SCOPE__', '/');
225
- result.push({ name, version });
226
- }
227
- }
228
- }
229
- return result;
230
- }
231
-
232
- module.exports = { request, getJSON, download, list };
246
+ module.exports = { request, getJSON, download };