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.
@@ -2,102 +2,148 @@
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 PackageJSON = require('../core/package-json');
6
7
  const { formatBytes } = require('../utils/fs');
7
- const logger = require('../utils/logger');
8
8
 
9
- module.exports = async function list(args, flags) {
10
- const cwd = process.cwd();
11
- const depth = parseInt(flags.depth ?? flags.d ?? '0', 10);
12
- const json = flags.json;
13
- const nodeModules = path.join(cwd, 'node_modules');
14
-
15
- const pkgJson = PackageJSON.fromDir(cwd);
16
-
17
- if (!fs.existsSync(nodeModules)) {
18
- logger.warn('No node_modules found. Run `jpm install` first.');
19
- return;
9
+ /**
10
+ * ListCommand handles the 'jpm list', 'jpm ls', and 'jpm peek' commands.
11
+ * It visualizes the installed dependency tree and package sizes.
12
+ */
13
+ class ListCommand extends BaseCommand {
14
+ constructor() {
15
+ super('list');
20
16
  }
21
17
 
22
- // Collect top-level installed packages
23
- const installed = [];
24
- for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) {
25
- if (entry.name.startsWith('.')) continue;
18
+ /**
19
+ * Executes the listing and visualization of dependencies.
20
+ *
21
+ * @param {string[]} args - Optional arguments
22
+ * @param {Object} flags - CLI flags (e.g., --depth, --json)
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ const cwd = process.cwd();
27
+ const depth = parseInt(flags.depth ?? flags.d ?? '0', 10);
28
+ const isJson = flags.json;
29
+ const nodeModules = path.join(cwd, 'node_modules');
30
+
31
+ const pkgJson = PackageJSON.fromDir(cwd);
32
+
33
+ if (!fs.existsSync(nodeModules)) {
34
+ this.logger.warn('No node_modules found. Run `jpm install` first.');
35
+ return;
36
+ }
26
37
 
27
- if (entry.name.startsWith('@') && entry.isDirectory()) {
28
- const scopeDir = path.join(nodeModules, entry.name);
29
- for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
30
- const pkg = readPkg(path.join(scopeDir, scoped.name));
38
+ // 1. Collect installed packages
39
+ const installed = [];
40
+ for (const entry of fs.readdirSync(nodeModules, { withFileTypes: true })) {
41
+ if (entry.name.startsWith('.')) continue;
42
+
43
+ if (entry.name.startsWith('@') && entry.isDirectory()) {
44
+ const scopeDir = path.join(nodeModules, entry.name);
45
+ for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
46
+ const pkg = this._readPkg(path.join(scopeDir, scoped.name));
47
+ if (pkg) installed.push(pkg);
48
+ }
49
+ } else if (entry.isDirectory()) {
50
+ const pkg = this._readPkg(path.join(nodeModules, entry.name));
31
51
  if (pkg) installed.push(pkg);
32
52
  }
33
- } else if (entry.isDirectory()) {
34
- const pkg = readPkg(path.join(nodeModules, entry.name));
35
- if (pkg) installed.push(pkg);
36
53
  }
37
- }
38
54
 
39
- // Sort alphabetically
40
- installed.sort((a, b) => a.name.localeCompare(b.name));
55
+ // 2. Sort results alphabetically
56
+ installed.sort((a, b) => a.name.localeCompare(b.name));
41
57
 
42
- if (json) {
43
- process.stdout.write(JSON.stringify({ name: pkgJson.name, dependencies: toObj(installed) }, null, 2) + '\n');
44
- return;
45
- }
58
+ // 3. Handle JSON output mode
59
+ if (isJson) {
60
+ process.stdout.write(JSON.stringify({
61
+ name: pkgJson.name,
62
+ dependencies: this._toObj(installed)
63
+ }, null, 2) + '\n');
64
+ return;
65
+ }
66
+
67
+ // 4. Render terminal tree view
68
+ this.logger.log(`\n${this.logger.c.bold(pkgJson.name)}@${pkgJson.version}`);
69
+
70
+ const directDeps = new Set([
71
+ ...Object.keys(pkgJson.dependencies),
72
+ ...Object.keys(pkgJson.devDependencies),
73
+ ]);
74
+
75
+ const total = installed.length;
76
+ let totalSize = 0;
77
+
78
+ for (let i = 0; i < installed.length; i++) {
79
+ const pkg = installed[i];
80
+ const isLast = i === installed.length - 1;
81
+ const devMark = Object.keys(pkgJson.devDependencies).includes(pkg.name)
82
+ ? this.logger.c.gray(' dev')
83
+ : '';
84
+
85
+ const conn = isLast ? '└── ' : '├── ';
86
+ const nameStr = this.logger.c.cyan(pkg.name);
87
+ const verStr = this.logger.c.gray(`@${pkg.version}`);
88
+ this.logger.log(`${conn}${nameStr}${verStr}${devMark}`);
89
+
90
+ if (depth > 0 && pkg.dependencies) {
91
+ const subDeps = Object.entries(pkg.dependencies);
92
+ subDeps.forEach(([depName, depRange], j) => {
93
+ const isLastSub = j === subDeps.length - 1;
94
+ const ext = isLast ? ' ' : '│ ';
95
+ const subConn = isLastSub ? '└── ' : '├── ';
96
+ this.logger.log(`${ext}${subConn}${this.logger.c.gray(depName)} ${this.logger.c.gray(depRange)}`);
97
+ });
98
+ }
46
99
 
47
- logger.log(`\n${logger.c.bold(pkgJson.name)}@${pkgJson.version}`);
48
-
49
- const directDeps = new Set([
50
- ...Object.keys(pkgJson.dependencies),
51
- ...Object.keys(pkgJson.devDependencies),
52
- ]);
53
-
54
- const total = installed.length;
55
- let totalSize = 0;
56
-
57
- for (let i = 0; i < installed.length; i++) {
58
- const pkg = installed[i];
59
- const isLast = i === installed.length - 1;
60
- const isDir = directDeps.has(pkg.name);
61
- const devMark = Object.keys(pkgJson.devDependencies).includes(pkg.name)
62
- ? logger.c.gray(' dev')
63
- : '';
64
-
65
- const conn = isLast ? '└── ' : '├── ';
66
- const nameStr = logger.c.cyan(pkg.name);
67
- const verStr = logger.c.gray(`@${pkg.version}`);
68
- logger.log(`${conn}${nameStr}${verStr}${devMark}`);
69
-
70
- if (depth > 0 && pkg.dependencies) {
71
- const subDeps = Object.entries(pkg.dependencies);
72
- subDeps.forEach(([depName, depRange], j) => {
73
- const isLastSub = j === subDeps.length - 1;
74
- const ext = isLast ? ' ' : '│ ';
75
- const subConn = isLastSub ? '└── ' : '├── ';
76
- logger.log(`${ext}${subConn}${logger.c.gray(depName)} ${logger.c.gray(depRange)}`);
77
- });
100
+ totalSize += pkg.size || 0;
78
101
  }
79
102
 
80
- totalSize += pkg.size || 0;
103
+ this.logger.log(`\n${total} packages ${formatBytes(totalSize)}`);
81
104
  }
82
105
 
83
- logger.log(`\n${total} packages ${formatBytes(totalSize)}`);
84
- };
85
-
86
- function readPkg(dir) {
87
- const f = path.join(dir, 'package.json');
88
- try {
89
- const data = JSON.parse(fs.readFileSync(f, 'utf8'));
90
- let size = 0;
106
+ /**
107
+ * Reads package metadata and calculates directory size.
108
+ *
109
+ * @param {string} dir - Directory path to the package
110
+ * @returns {Object|null} Package data object or null on failure
111
+ * @private
112
+ */
113
+ _readPkg(dir) {
114
+ const pkgJsonFile = path.join(dir, 'package.json');
91
115
  try {
92
- for (const file of fs.readdirSync(dir)) {
93
- const s = fs.statSync(path.join(dir, file));
94
- if (s.isFile()) size += s.size;
95
- }
96
- } catch { }
97
- return { name: data.name, version: data.version, dependencies: data.dependencies, size };
98
- } catch { return null; }
99
- }
116
+ const data = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf8'));
117
+ let size = 0;
118
+ try {
119
+ // Shallow size calculation (top-level files only)
120
+ for (const file of fs.readdirSync(dir)) {
121
+ const s = fs.statSync(path.join(dir, file));
122
+ if (s.isFile()) size += s.size;
123
+ }
124
+ } catch (err) { }
125
+ return {
126
+ name: data.name,
127
+ version: data.version,
128
+ dependencies: data.dependencies,
129
+ size
130
+ };
131
+ } catch (err) {
132
+ return null;
133
+ }
134
+ }
100
135
 
101
- function toObj(arr) {
102
- return Object.fromEntries(arr.map(p => [p.name, { version: p.version }]));
136
+ /**
137
+ * Converts an array of package objects into a structured object for JSON output.
138
+ *
139
+ * @param {Object[]} arr - Array of package metadata
140
+ * @returns {Object}
141
+ * @private
142
+ */
143
+ _toObj(arr) {
144
+ return Object.fromEntries(arr.map(p => [p.name, { version: p.version }]));
145
+ }
103
146
  }
147
+
148
+ module.exports = ListCommand;
149
+
@@ -2,147 +2,178 @@
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 tar = require('tar');
6
+ const BaseCommand = require('./base-command');
7
7
  const PackageJSON = require('../core/package-json');
8
8
  const integrity = require('../security/integrity');
9
- const { getJSON } = require('../utils/http');
10
- const { tempDir, mkdirp } = require('../utils/fs');
9
+ const { getJSON, request } = require('../utils/http');
10
+ const { tempDir } = require('../utils/fs');
11
11
  const { Spinner } = require('../utils/progress');
12
- const logger = require('../utils/logger');
13
- const config = require('../utils/config');
14
-
15
- module.exports = async function publish(args, flags) {
16
- const cwd = process.cwd();
17
- const pkgJson = PackageJSON.fromDir(cwd);
18
-
19
- // 1. Validate
20
- const errors = pkgJson.validate();
21
- if (errors.length) {
22
- logger.error('package.json validation failed:');
23
- errors.forEach(e => logger.error(` ${e}`));
24
- process.exit(1);
12
+
13
+ /**
14
+ * PublishCommand handles the 'jpm publish' and 'jpm ship' commands.
15
+ * It validates a package, packs it into a tarball, and uploads it to the registry.
16
+ */
17
+ class PublishCommand extends BaseCommand {
18
+ constructor() {
19
+ super('publish');
25
20
  }
26
21
 
27
- const { name, version } = pkgJson;
28
- const registry = config.registry.replace(/\/$/, '');
22
+ /**
23
+ * Executes the package publishing process.
24
+ *
25
+ * @param {string[]} args - Optional arguments
26
+ * @param {Object} flags - CLI flags (e.g., --otp)
27
+ * @returns {Promise<void>}
28
+ */
29
+ async run(args, flags) {
30
+ const cwd = process.cwd();
31
+ const pkgJson = PackageJSON.fromDir(cwd);
32
+
33
+ // 1. Validate package configuration
34
+ const errors = pkgJson.validate();
35
+ if (errors.length) {
36
+ this.logger.error('package.json validation failed:');
37
+ errors.forEach(e => this.logger.error(` ${e}`));
38
+ throw new Error('Package validation failed.');
39
+ }
29
40
 
30
- logger.section(`Publishing ${name}@${version}`);
41
+ const { name, version } = pkgJson;
42
+ const registryUrl = this.config.registry.replace(/\/$/, '');
31
43
 
32
- // 2. Check auth token
33
- const token = config.get('//registry.npmjs.org/:_authToken') || config.get('_authToken');
34
- if (!token) {
35
- logger.error('Not logged in. Set _authToken in ~/.jpmrc or run: npm login');
36
- process.exit(1);
37
- }
44
+ this.logger.section(`Publishing ${name}@${version}`);
38
45
 
39
- // 3. Check if version already exists
40
- const spinner = new Spinner('Checking if version exists...').start();
41
- try {
42
- const existing = await getJSON(`${registry}/${encodeURIComponent(name)}/${version}`);
43
- spinner.fail(`Version ${name}@${version} already exists in registry`);
44
- process.exit(1);
45
- } catch (err) {
46
- if (err.status !== 404) {
47
- spinner.warn('Could not verify version uniqueness, continuing...');
48
- } else {
49
- spinner.succeed('Version check passed');
46
+ // 2. Resolve authentication token
47
+ const token = this.config.get('//registry.npmjs.org/:_authToken') || this.config.get('_authToken');
48
+ if (!token) {
49
+ this.logger.error('Not logged in. Set _authToken in ~/.jpmrc or run: npm login');
50
+ throw new Error('Authentication required.');
50
51
  }
51
- }
52
52
 
53
- // 4. Create tarball
54
- const tmp = tempDir('jpm-publish-');
55
- const tgz = path.join(tmp, `${name.replace('@', '').replace('/', '-')}-${version}.tgz`);
56
-
57
- const packSpinner = new Spinner('Packing tarball...').start();
58
-
59
- // Collect files respecting .npmignore / default ignore list
60
- const ignore = getIgnoreList(cwd);
61
- await tar.create(
62
- { gzip: true, file: tgz, cwd, prefix: 'package' },
63
- getFilesToPack(cwd, ignore)
64
- );
65
-
66
- const tgzStat = fs.statSync(tgz);
67
- const shasum = await integrity.hashFile(tgz, 'sha1', 'hex');
68
- const integrityH = await integrity.generateIntegrity(tgz);
69
- const tgzBase64 = fs.readFileSync(tgz).toString('base64');
70
-
71
- packSpinner.succeed(`Packed ${logger.c.gray(`(${(tgzStat.size / 1024).toFixed(1)} KB)`)}`);
72
-
73
- // 5. Build publish body
74
- const body = {
75
- _id: name,
76
- name,
77
- 'dist-tags': { latest: version },
78
- versions: {
79
- [version]: {
80
- ...pkgJson.data,
81
- dist: {
82
- shasum,
83
- integrity: integrityH,
84
- tarball: `${registry}/${encodeURIComponent(name)}/-/${name}-${version}.tgz`,
53
+ // 3. Prevent overwriting existing versions
54
+ const spinner = new Spinner('Checking if version exists...').start();
55
+ try {
56
+ await getJSON(`${registryUrl}/${encodeURIComponent(name)}/${version}`);
57
+ spinner.fail(`Version ${name}@${version} already exists in registry`);
58
+ throw new Error(`Version ${version} already exists.`);
59
+ } catch (err) {
60
+ if (err.status !== 404) {
61
+ spinner.warn('Could not verify version uniqueness, continuing...');
62
+ } else {
63
+ spinner.succeed('Version check passed');
64
+ }
65
+ }
66
+
67
+ // 4. Create and pack tarball
68
+ const tmp = tempDir('jpm-publish-');
69
+ const tgz = path.join(tmp, `${name.replace('@', '').replace('/', '-')}-${version}.tgz`);
70
+
71
+ const packSpinner = new Spinner('Packing tarball...').start();
72
+ const ignore = this._getIgnoreList(cwd);
73
+
74
+ await tar.create(
75
+ { gzip: true, file: tgz, cwd, prefix: 'package' },
76
+ this._getFilesToPack(cwd, ignore)
77
+ );
78
+
79
+ const tgzStat = fs.statSync(tgz);
80
+ const shasum = await integrity.hashFile(tgz, 'sha1', 'hex');
81
+ const integrityH = await integrity.generateIntegrity(tgz);
82
+ const tgzBase64 = fs.readFileSync(tgz).toString('base64');
83
+
84
+ packSpinner.succeed(`Packed ${this.logger.c.gray(`(${(tgzStat.size / 1024).toFixed(1)} KB)`)}`);
85
+
86
+ // 5. Construct registry payload
87
+ const body = {
88
+ _id: name,
89
+ name,
90
+ 'dist-tags': { latest: version },
91
+ versions: {
92
+ [version]: {
93
+ ...pkgJson.data,
94
+ dist: {
95
+ shasum,
96
+ integrity: integrityH,
97
+ tarball: `${registryUrl}/${encodeURIComponent(name)}/-/${name}-${version}.tgz`,
98
+ },
85
99
  },
86
100
  },
87
- },
88
- _attachments: {
89
- [`${name}-${version}.tgz`]: {
90
- content_type: 'application/octet-stream',
91
- data: tgzBase64,
92
- length: tgzStat.size,
93
- },
94
- },
95
- };
96
-
97
- // 6. PUT to registry
98
- const uploadSpinner = new Spinner('Uploading to registry...').start();
99
- const { request } = require('../utils/http');
100
- const bodyStr = JSON.stringify(body);
101
-
102
- try {
103
- const res = await request(`${registry}/${encodeURIComponent(name)}`, {
104
- method: 'PUT',
105
- headers: {
106
- 'Content-Type': 'application/json',
107
- 'Authorization': `Bearer ${token}`,
108
- 'Content-Length': Buffer.byteLength(bodyStr),
109
- ...(flags.otp ? { 'npm-otp': flags.otp } : {}),
101
+ _attachments: {
102
+ [`${name}-${version}.tgz`]: {
103
+ content_type: 'application/octet-stream',
104
+ data: tgzBase64,
105
+ length: tgzStat.size,
106
+ },
110
107
  },
111
- body: bodyStr,
112
- retries: 1,
113
- });
114
-
115
- if (res.status < 200 || res.status >= 300) {
116
- uploadSpinner.fail(`Publish failed: HTTP ${res.status} — ${res.body.slice(0, 200)}`);
117
- process.exit(1);
108
+ };
109
+
110
+ // 6. Submit to registry
111
+ const uploadSpinner = new Spinner('Uploading to registry...').start();
112
+ const bodyStr = JSON.stringify(body);
113
+
114
+ try {
115
+ const res = await request(`${registryUrl}/${encodeURIComponent(name)}`, {
116
+ method: 'PUT',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ 'Authorization': `Bearer ${token}`,
120
+ 'Content-Length': Buffer.byteLength(bodyStr),
121
+ ...(flags.otp ? { 'npm-otp': flags.otp } : {}),
122
+ },
123
+ body: bodyStr,
124
+ retries: 1,
125
+ });
126
+
127
+ if (res.status < 200 || res.status >= 300) {
128
+ uploadSpinner.fail(`Publish failed: HTTP ${res.status} — ${res.body.slice(0, 200)}`);
129
+ throw new Error(`Registry responded with status ${res.status}`);
130
+ }
131
+
132
+ uploadSpinner.succeed('Published!');
133
+ this.logger.success(`\n+ ${name}@${version}`);
134
+ this.logger.log(this.logger.c.gray(`${registryUrl}/${encodeURIComponent(name)}`));
135
+ } catch (err) {
136
+ uploadSpinner.fail(`Publish error: ${err.message}`);
137
+ throw err;
118
138
  }
119
-
120
- uploadSpinner.succeed('Published!');
121
- logger.success(`\n+ ${name}@${version}`);
122
- logger.log(logger.c.gray(`${registry}/${encodeURIComponent(name)}`));
123
- } catch (err) {
124
- uploadSpinner.fail(`Publish error: ${err.message}`);
125
- process.exit(1);
126
139
  }
127
- };
128
-
129
- function getIgnoreList(dir) {
130
- const defaults = new Set(['node_modules', '.git', '.DS_Store', '*.log', 'coverage', '.jpm-lock.json']);
131
- const ignoreFile = path.join(dir, '.npmignore');
132
- if (fs.existsSync(ignoreFile)) {
133
- fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(l => {
134
- const t = l.trim();
135
- if (t && !t.startsWith('#')) defaults.add(t);
136
- });
140
+
141
+ /**
142
+ * Generates a list of files to ignore during packing.
143
+ *
144
+ * @param {string} dir - Project root directory
145
+ * @returns {Set<string>} Set of ignored patterns/filenames
146
+ * @private
147
+ */
148
+ _getIgnoreList(dir) {
149
+ const defaults = new Set(['node_modules', '.git', '.DS_Store', '*.log', 'coverage', '.jpm-lock.json']);
150
+ const ignoreFile = path.join(dir, '.npmignore');
151
+ if (fs.existsSync(ignoreFile)) {
152
+ fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(l => {
153
+ const t = l.trim();
154
+ if (t && !t.startsWith('#')) defaults.add(t);
155
+ });
156
+ }
157
+ return defaults;
137
158
  }
138
- return defaults;
139
- }
140
159
 
141
- function getFilesToPack(dir, ignore) {
142
- const files = [];
143
- for (const entry of fs.readdirSync(dir)) {
144
- if ([...ignore].some(ig => entry === ig || entry.startsWith(ig.replace('*', '')))) continue;
145
- files.push(entry);
160
+ /**
161
+ * Scans the directory for files that should be included in the tarball.
162
+ *
163
+ * @param {string} dir - Directory to scan
164
+ * @param {Set<string>} ignore - Set of ignored patterns
165
+ * @returns {string[]} List of files to pack
166
+ * @private
167
+ */
168
+ _getFilesToPack(dir, ignore) {
169
+ const files = [];
170
+ for (const entry of fs.readdirSync(dir)) {
171
+ if ([...ignore].some(ig => entry === ig || entry.startsWith(ig.replace('*', '')))) continue;
172
+ files.push(entry);
173
+ }
174
+ return files.length ? files : ['.'];
146
175
  }
147
- return files.length ? files : ['.'];
148
176
  }
177
+
178
+ module.exports = PublishCommand;
179
+