jpm-pkg 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/jpm.js +7 -1
- package/package.json +1 -1
- package/src/commands/audit.js +70 -41
- package/src/commands/base-command.js +62 -0
- package/src/commands/config.js +94 -51
- package/src/commands/info.js +111 -64
- package/src/commands/init.js +117 -72
- package/src/commands/install.js +121 -112
- package/src/commands/list.js +126 -80
- package/src/commands/publish.js +155 -124
- package/src/commands/run.js +86 -52
- package/src/commands/search.js +56 -32
- package/src/commands/uninstall.js +47 -30
- package/src/commands/update.js +92 -51
- package/src/commands/x.js +134 -96
- package/src/core/cache.js +188 -87
- package/src/core/lockfile.js +73 -26
- package/src/core/package-json.js +119 -1
- package/src/core/registry.js +182 -143
- package/src/core/resolver.js +60 -36
package/src/commands/x.js
CHANGED
|
@@ -1,136 +1,174 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const BaseCommand = require('./base-command');
|
|
5
7
|
const Resolver = require('../core/resolver');
|
|
6
8
|
const Installer = require('../core/installer');
|
|
7
9
|
const { tempDir, rimraf } = require('../utils/fs');
|
|
8
10
|
const { Spinner } = require('../utils/progress');
|
|
9
|
-
const config = require('../utils/config');
|
|
10
|
-
const logger = require('../utils/logger');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* jpm x
|
|
14
|
-
*
|
|
15
|
-
* Equivalent to npx. Resolves, installs to a temp dir, and executes.
|
|
13
|
+
* ExecCommand handles the 'jpm x' and 'jpm exec' commands (equivalent to npx).
|
|
14
|
+
* It resolves a package, installs it to a temporary directory, and executes its binary.
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger.info('Usage: jpm x <package>[@version] [args...]');
|
|
21
|
-
process.exit(1);
|
|
16
|
+
class ExecCommand extends BaseCommand {
|
|
17
|
+
constructor() {
|
|
18
|
+
super('x');
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
name = hasVersion ? pkgArg.slice(0, lastAt) : pkgArg;
|
|
38
|
-
range = hasVersion ? pkgArg.slice(lastAt + 1) : 'latest';
|
|
39
|
-
}
|
|
21
|
+
/**
|
|
22
|
+
* Executes the specified package binary.
|
|
23
|
+
*
|
|
24
|
+
* @param {string[]} args - Package name and arguments for the binary
|
|
25
|
+
* @param {Object} flags - CLI flags
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async run(args, flags) {
|
|
29
|
+
if (!args.length) {
|
|
30
|
+
this.logger.error('No package specified to execute.');
|
|
31
|
+
this.logger.info('Usage: jpm x <package>[@version] [args...]');
|
|
32
|
+
throw new Error('No package specified.');
|
|
33
|
+
}
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
const pkgArg = args[0];
|
|
36
|
+
const execArgs = args.slice(1);
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages`);
|
|
38
|
+
// Parse name and range from argument (handling scoped packages)
|
|
39
|
+
let name, range;
|
|
40
|
+
const lastAt = pkgArg.lastIndexOf('@');
|
|
41
|
+
const isScoped = pkgArg.startsWith('@');
|
|
42
|
+
const hasVersion = lastAt > 0 && !(isScoped && pkgArg.indexOf('@', 1) === -1);
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
if (isScoped) {
|
|
45
|
+
const parts = pkgArg.split('@');
|
|
46
|
+
name = '@' + parts[1];
|
|
47
|
+
range = parts[2] || 'latest';
|
|
48
|
+
} else {
|
|
49
|
+
name = hasVersion ? pkgArg.slice(0, lastAt) : pkgArg;
|
|
50
|
+
range = hasVersion ? pkgArg.slice(lastAt + 1) : 'latest';
|
|
51
|
+
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
await installer.installAll(resolvedMap, { flags });
|
|
53
|
+
const resolveSpinner = new Spinner(`Resolving ${name}@${range}...`).start();
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
let tmpDir;
|
|
56
|
+
try {
|
|
57
|
+
// 1. Resolve dependencies
|
|
58
|
+
const resolver = new Resolver();
|
|
59
|
+
const resolvedMap = await resolver.resolve({ [name]: range }, {}, {}, (count) => {
|
|
60
|
+
resolveSpinner.text = `Resolving... (${count} resolved)`;
|
|
61
|
+
});
|
|
62
|
+
resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages`);
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// 2. Install to temporary directory
|
|
65
|
+
tmpDir = tempDir('jpm-x-');
|
|
66
|
+
const installer = new Installer(tmpDir);
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} else if (typeof bins === 'object' && bins !== null) {
|
|
68
|
-
const entries = Object.keys(bins);
|
|
69
|
-
if (entries.length === 0) throw new Error(`Package ${name} has no binaries.`);
|
|
70
|
-
binName = entries.find(k => k === name || k === name.split('/').pop()) || entries[0];
|
|
71
|
-
} else {
|
|
72
|
-
throw new Error(`Package ${name} does not define any binaries in package.json`);
|
|
73
|
-
}
|
|
68
|
+
this.logger.info(`Installing to temporary directory...`);
|
|
69
|
+
await installer.installAll(resolvedMap, { flags });
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
// 3. Locate and execute binary
|
|
72
|
+
const resolvedPkg = [...resolvedMap.values()].find(m => m.name === name);
|
|
73
|
+
if (!resolvedPkg) {
|
|
74
|
+
throw new Error(`Failed to find resolved metadata for ${name}`);
|
|
75
|
+
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let finalArgs = execArgs;
|
|
80
|
-
let useNode = false;
|
|
77
|
+
const binaryInfo = this._resolveBinary(name, resolvedPkg.bin);
|
|
78
|
+
const binaryPath = path.join(tmpDir, 'node_modules', '.bin', binaryInfo.name + (process.platform === 'win32' ? '.cmd' : ''));
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
let finalExecutable = binaryPath;
|
|
81
|
+
let finalArgs = execArgs;
|
|
82
|
+
let useNode = false;
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(binaryPath)) {
|
|
85
|
+
// Fallback: search for the JS file defined in package.json bin field
|
|
86
|
+
const relPath = typeof resolvedPkg.bin === 'string' ? resolvedPkg.bin : resolvedPkg.bin[binaryInfo.name];
|
|
87
|
+
finalExecutable = path.join(tmpDir, 'node_modules', name, relPath);
|
|
88
|
+
useNode = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.logger.info(`Executing ${binaryInfo.name}...\n`);
|
|
92
|
+
|
|
93
|
+
const env = {
|
|
94
|
+
...process.env,
|
|
95
|
+
PATH: `${path.join(tmpDir, 'node_modules', '.bin')}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`,
|
|
96
|
+
};
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
const result = this._spawnProcess(finalExecutable, finalArgs, useNode, env);
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
const spawnArgs = useNode ? [finalBin, ...execArgs] : execArgs;
|
|
100
|
+
if (result.error) throw result.error;
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
// Cleanup temp directory on success unless debugging
|
|
103
|
+
if (result.status === 0 && this.config.loglevel !== 'debug') {
|
|
104
|
+
rimraf(tmpDir);
|
|
105
|
+
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
process.exit(result.status ?? 0);
|
|
108
|
+
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (tmpDir && fs.existsSync(tmpDir) && this.config.loglevel !== 'debug') {
|
|
111
|
+
rimraf(tmpDir);
|
|
112
|
+
}
|
|
113
|
+
resolveSpinner.fail(`Error: ${err.message}`);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Identifies the primary binary name from package metadata.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} pkgName - Package name
|
|
122
|
+
* @param {string|Object} binField - The 'bin' field from package.json
|
|
123
|
+
* @returns {Object} Binary name and relative path
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_resolveBinary(pkgName, binField) {
|
|
127
|
+
let name;
|
|
128
|
+
if (typeof binField === 'string') {
|
|
129
|
+
name = pkgName.split('/').pop();
|
|
130
|
+
return { name, path: binField };
|
|
131
|
+
} else if (typeof binField === 'object' && binField !== null) {
|
|
132
|
+
const entries = Object.keys(binField);
|
|
133
|
+
if (entries.length === 0) throw new Error(`Package ${pkgName} has no binaries.`);
|
|
134
|
+
name = entries.find(k => k === pkgName || k === pkgName.split('/').pop()) || entries[0];
|
|
135
|
+
return { name, path: binField[name] };
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`Package ${pkgName} defines no binaries.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Spawns the execution process with cross-platform considerations.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} bin - Binary/Executable path
|
|
144
|
+
* @param {string[]} args - Arguments
|
|
145
|
+
* @param {boolean} useNode - Whether to execute via the node binary
|
|
146
|
+
* @param {Object} env - Environment variables
|
|
147
|
+
* @returns {import('node:child_process').SpawnSyncReturns<Buffer>}
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_spawnProcess(bin, args, useNode, env) {
|
|
102
151
|
const isWin = process.platform === 'win32';
|
|
103
152
|
|
|
104
|
-
let result;
|
|
105
153
|
if (isWin && useNode) {
|
|
106
|
-
//
|
|
107
|
-
|
|
154
|
+
// Windows specific: wrap in quotes and use verbatim arguments for reliability
|
|
155
|
+
return spawnSync(`"${process.execPath}"`, [`"${bin}"`, ...args.map(a => `"${a}"`)], {
|
|
108
156
|
cwd: process.cwd(),
|
|
109
157
|
stdio: 'inherit',
|
|
110
158
|
env,
|
|
111
159
|
shell: true,
|
|
112
160
|
windowsVerbatimArguments: true
|
|
113
161
|
});
|
|
114
|
-
} else {
|
|
115
|
-
result = spawnSync(useNode ? process.execPath : finalBin, useNode ? [finalBin, ...execArgs] : execArgs, {
|
|
116
|
-
cwd: process.cwd(),
|
|
117
|
-
stdio: 'inherit',
|
|
118
|
-
env,
|
|
119
|
-
shell: true
|
|
120
|
-
});
|
|
121
162
|
}
|
|
122
163
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
164
|
+
return spawnSync(useNode ? process.execPath : bin, useNode ? [bin, ...args] : args, {
|
|
165
|
+
cwd: process.cwd(),
|
|
166
|
+
stdio: 'inherit',
|
|
167
|
+
env,
|
|
168
|
+
shell: true
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
129
172
|
|
|
130
|
-
|
|
173
|
+
module.exports = ExecCommand;
|
|
131
174
|
|
|
132
|
-
} catch (err) {
|
|
133
|
-
resolveSpinner.fail(`Error: ${err.message}`);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
};
|
package/src/core/cache.js
CHANGED
|
@@ -2,116 +2,217 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const crypto = require('node:crypto');
|
|
6
5
|
const config = require('../utils/config');
|
|
7
6
|
const { mkdirp, rimraf } = require('../utils/fs');
|
|
8
7
|
const logger = require('../utils/logger');
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Cache class handles persistent disk caching of package tarballs and metadata.
|
|
11
|
+
* Packages are stored at `<cacheDir>/<name>/<version>.tgz`.
|
|
12
|
+
* Scoped packages handle '@' by replacing '/' with '__SCOPE__'.
|
|
13
13
|
*/
|
|
14
|
+
class Cache {
|
|
15
|
+
/**
|
|
16
|
+
* Creates an instance of the Cache.
|
|
17
|
+
* @param {Object} [options={}] - Configuration options for the cache
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this._config = options.config || config;
|
|
21
|
+
}
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns the root directory for the cache.
|
|
25
|
+
* @returns {string} Absolute path to the cache directory
|
|
26
|
+
*/
|
|
27
|
+
get cacheRoot() {
|
|
28
|
+
return this._config.cacheDir;
|
|
29
|
+
}
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the absolute path for a package tarball in the cache.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} name - The package name
|
|
35
|
+
* @param {string} version - The package version
|
|
36
|
+
* @returns {string} Absolute path to the .tgz file
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
_tgzPath(name, version) {
|
|
40
|
+
const safeName = name.replace('/', '__SCOPE__');
|
|
41
|
+
return path.join(this.cacheRoot, safeName, `${version}.tgz`);
|
|
42
|
+
}
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the absolute path for a package metadata JSON in the cache.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} name - The package name
|
|
48
|
+
* @param {string} version - The package version
|
|
49
|
+
* @returns {string} Absolute path to the .json file
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
_metaPath(name, version) {
|
|
53
|
+
const safeName = name.replace('/', '__SCOPE__');
|
|
54
|
+
return path.join(this.cacheRoot, safeName, `${version}.json`);
|
|
34
55
|
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
57
|
+
/**
|
|
58
|
+
* Retrieves a package tarball from the cache if it exists.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} name - Package name
|
|
61
|
+
* @param {string} version - Package version
|
|
62
|
+
* @returns {Promise<string|null>} Path to the cached tarball, or null if not found
|
|
63
|
+
*/
|
|
64
|
+
async get(name, version) {
|
|
65
|
+
const p = this._tgzPath(name, version);
|
|
66
|
+
if (fs.existsSync(p)) {
|
|
67
|
+
logger.verbose(`cache hit ${name}@${version}`);
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
44
72
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
73
|
+
/**
|
|
74
|
+
* Stores a package tarball in the cache.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name - Package name
|
|
77
|
+
* @param {string} version - Package version
|
|
78
|
+
* @param {string} srcTgz - Path to the source tarball to be copied
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
async set(name, version, srcTgz) {
|
|
82
|
+
const dest = this._tgzPath(name, version);
|
|
83
|
+
mkdirp(path.dirname(dest));
|
|
84
|
+
fs.copyFileSync(srcTgz, dest);
|
|
85
|
+
logger.verbose(`cache store ${name}@${version}`);
|
|
86
|
+
}
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
88
|
+
/**
|
|
89
|
+
* Stores package metadata in the cache.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} name - Package name
|
|
92
|
+
* @param {string} version - Package version
|
|
93
|
+
* @param {Object} meta - Metadata object to be serialized
|
|
94
|
+
* @returns {Promise<void>}
|
|
95
|
+
*/
|
|
96
|
+
async setMeta(name, version, meta) {
|
|
97
|
+
const p = this._metaPath(name, version);
|
|
98
|
+
mkdirp(path.dirname(p));
|
|
99
|
+
fs.writeFileSync(p, JSON.stringify(meta, null, 2), 'utf8');
|
|
100
|
+
}
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves package metadata from the cache.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} name - Package name
|
|
106
|
+
* @param {string} version - Package version
|
|
107
|
+
* @returns {Promise<Object|null>} Decoded metadata object, or null if not found/invalid
|
|
108
|
+
*/
|
|
109
|
+
async getMeta(name, version) {
|
|
110
|
+
const p = this._metaPath(name, version);
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
rimraf(cacheRoot());
|
|
71
|
-
logger.success('Cache cleared');
|
|
118
|
+
/**
|
|
119
|
+
* Checks if a package version exists in the cache.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} name - Package name
|
|
122
|
+
* @param {string} version - Package version
|
|
123
|
+
* @returns {boolean} True if the tarball exists in cache
|
|
124
|
+
*/
|
|
125
|
+
has(name, version) {
|
|
126
|
+
return fs.existsSync(this._tgzPath(name, version));
|
|
72
127
|
}
|
|
73
|
-
}
|
|
74
128
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Clears specific items or the entire cache.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} [name] - Optional package name to clear
|
|
133
|
+
* @param {string} [version] - Optional version to clear (requires name)
|
|
134
|
+
*/
|
|
135
|
+
clear(name, version) {
|
|
136
|
+
if (name && version) {
|
|
137
|
+
rimraf(this._tgzPath(name, version));
|
|
138
|
+
rimraf(this._metaPath(name, version));
|
|
139
|
+
} else if (name) {
|
|
140
|
+
const safeName = name.replace('/', '__SCOPE__');
|
|
141
|
+
rimraf(path.join(this.cacheRoot, safeName));
|
|
142
|
+
} else {
|
|
143
|
+
// Clear entire cache
|
|
144
|
+
rimraf(this.cacheRoot);
|
|
145
|
+
logger.success('Cache cleared');
|
|
92
146
|
}
|
|
93
147
|
}
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Calculates cache statistics (total packages and disk usage).
|
|
151
|
+
*
|
|
152
|
+
* @returns {Object} { packages: number, size: number, root: string }
|
|
153
|
+
*/
|
|
154
|
+
stats() {
|
|
155
|
+
const root = this.cacheRoot;
|
|
156
|
+
if (!fs.existsSync(root)) return { packages: 0, size: 0, root };
|
|
157
|
+
|
|
158
|
+
let packages = 0;
|
|
159
|
+
let size = 0;
|
|
160
|
+
|
|
161
|
+
const walk = (dir) => {
|
|
162
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
163
|
+
const full = path.join(dir, entry.name);
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
walk(full);
|
|
166
|
+
} else {
|
|
167
|
+
const s = fs.statSync(full);
|
|
168
|
+
size += s.size;
|
|
169
|
+
if (entry.name.endsWith('.tgz')) packages++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
98
173
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
174
|
+
try {
|
|
175
|
+
walk(root);
|
|
176
|
+
} catch { }
|
|
177
|
+
return { packages, size, root };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Lists all packages and versions currently in the cache.
|
|
182
|
+
*
|
|
183
|
+
* @returns {{ name: string, version: string }[]} Array of package descriptors
|
|
184
|
+
*/
|
|
185
|
+
list() {
|
|
186
|
+
const root = this.cacheRoot;
|
|
187
|
+
if (!fs.existsSync(root)) return [];
|
|
188
|
+
const result = [];
|
|
189
|
+
for (const scopeOrName of fs.readdirSync(root)) {
|
|
190
|
+
const dir = path.join(root, scopeOrName);
|
|
191
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
192
|
+
for (const file of fs.readdirSync(dir)) {
|
|
193
|
+
if (file.endsWith('.tgz')) {
|
|
194
|
+
const version = file.replace('.tgz', '');
|
|
195
|
+
const name = scopeOrName.replace('__SCOPE__', '/');
|
|
196
|
+
result.push({ name, version });
|
|
197
|
+
}
|
|
111
198
|
}
|
|
112
199
|
}
|
|
200
|
+
return result;
|
|
113
201
|
}
|
|
114
|
-
return result;
|
|
115
202
|
}
|
|
116
203
|
|
|
117
|
-
|
|
204
|
+
// Singleton instance for backward compatibility
|
|
205
|
+
const defaultCache = new Cache();
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
Cache,
|
|
209
|
+
get: defaultCache.get.bind(defaultCache),
|
|
210
|
+
set: defaultCache.set.bind(defaultCache),
|
|
211
|
+
getMeta: defaultCache.getMeta.bind(defaultCache),
|
|
212
|
+
setMeta: defaultCache.setMeta.bind(defaultCache),
|
|
213
|
+
has: defaultCache.has.bind(defaultCache),
|
|
214
|
+
clear: defaultCache.clear.bind(defaultCache),
|
|
215
|
+
stats: defaultCache.stats.bind(defaultCache),
|
|
216
|
+
list: defaultCache.list.bind(defaultCache),
|
|
217
|
+
};
|
|
218
|
+
|