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.
@@ -3,86 +3,131 @@
3
3
  const readline = require('node:readline');
4
4
  const path = require('node:path');
5
5
  const os = require('node:os');
6
+ const BaseCommand = require('./base-command');
6
7
  const PackageJSON = require('../core/package-json');
7
- const logger = require('../utils/logger');
8
8
 
9
- module.exports = async function init(args, flags) {
10
- const cwd = process.cwd();
11
- const pkgFile = path.join(cwd, 'package.json');
12
- const pkg = new PackageJSON(pkgFile);
13
-
14
- if (flags.y || flags.yes) {
15
- // Non-interactive: write defaults
16
- pkg.save();
17
- logger.success(`Created package.json`);
18
- return;
9
+ /**
10
+ * InitCommand handles the 'jpm init' and 'jpm setup' commands.
11
+ * It initializes a new package.json file, either interactively or with defaults.
12
+ */
13
+ class InitCommand extends BaseCommand {
14
+ constructor() {
15
+ super('init');
19
16
  }
20
17
 
21
- logger.section('JPM Init — create a new package.json');
22
- logger.log(logger.c.gray('Press Enter to accept defaults shown in parentheses.\n'));
23
-
24
- const existing = pkg.data;
25
-
26
- const answers = await prompt([
27
- { key: 'name', label: 'Package name', default: existing.name || path.basename(cwd) },
28
- { key: 'version', label: 'Version', default: existing.version || '1.0.0' },
29
- { key: 'description', label: 'Description', default: existing.description || '' },
30
- { key: 'main', label: 'Entry point', default: existing.main || 'index.js' },
31
- { key: 'author', label: 'Author', default: existing.author || os.userInfo().username },
32
- { key: 'license', label: 'License', default: existing.license || 'MIT' },
33
- ]);
34
-
35
- const data = {
36
- name: answers.name,
37
- version: answers.version,
38
- description: answers.description,
39
- main: answers.main,
40
- scripts: existing.scripts || { test: 'echo "Error: no test specified" && exit 1' },
41
- keywords: existing.keywords || [],
42
- author: answers.author,
43
- license: answers.license,
44
- dependencies: existing.dependencies || {},
45
- devDependencies: existing.devDependencies || {},
46
- };
47
-
48
- // Strip empty strings from output
49
- for (const [k, v] of Object.entries(data)) {
50
- if (v === '') delete data[k];
18
+ /**
19
+ * Executes the initialization process.
20
+ *
21
+ * @param {string[]} args - Optional arguments
22
+ * @param {Object} flags - CLI flags (e.g., -y for defaults)
23
+ * @returns {Promise<void>}
24
+ */
25
+ async run(args, flags) {
26
+ const cwd = process.cwd();
27
+ const pkgFile = path.join(cwd, 'package.json');
28
+ const pkg = new PackageJSON(pkgFile);
29
+
30
+ if (flags.y || flags.yes) {
31
+ // Non-interactive: write defaults immediately
32
+ pkg.save();
33
+ this.logger.success(`Created package.json`);
34
+ return;
35
+ }
36
+
37
+ this.logger.section('JPM Init create a new package.json');
38
+ this.logger.log(this.logger.c.gray('Press Enter to accept defaults shown in parentheses.\n'));
39
+
40
+ const existing = pkg.data;
41
+
42
+ const answers = await this._prompt([
43
+ { key: 'name', label: 'Package name', default: existing.name || path.basename(cwd) },
44
+ { key: 'version', label: 'Version', default: existing.version || '1.0.0' },
45
+ { key: 'description', label: 'Description', default: existing.description || '' },
46
+ { key: 'main', label: 'Entry point', default: existing.main || 'index.js' },
47
+ { key: 'author', label: 'Author', default: existing.author || os.userInfo().username },
48
+ { key: 'license', label: 'License', default: existing.license || 'MIT' },
49
+ ]);
50
+
51
+ const data = {
52
+ name: answers.name,
53
+ version: answers.version,
54
+ description: answers.description,
55
+ main: answers.main,
56
+ scripts: existing.scripts || { test: 'echo "Error: no test specified" && exit 1' },
57
+ keywords: existing.keywords || [],
58
+ author: answers.author,
59
+ license: answers.license,
60
+ dependencies: existing.dependencies || {},
61
+ devDependencies: existing.devDependencies || {},
62
+ };
63
+
64
+ // Strip empty strings from output to keep it clean
65
+ for (const [k, v] of Object.entries(data)) {
66
+ if (v === '') delete data[k];
67
+ }
68
+
69
+ this.logger.log('\n' + JSON.stringify(data, null, 2));
70
+ const confirm = await this._ask('\nIs this OK? (yes) ');
71
+
72
+ if (confirm.toLowerCase() === 'no' || confirm.toLowerCase() === 'n') {
73
+ this.logger.warn('Aborted.');
74
+ return;
75
+ }
76
+
77
+ for (const [k, v] of Object.entries(data)) {
78
+ pkg.setField(k, v);
79
+ }
80
+
81
+ pkg.save();
82
+ this.logger.success(`\nWrote to ${pkgFile}`);
51
83
  }
52
84
 
53
- logger.log('\n' + JSON.stringify(data, null, 2));
54
- const confirm = await ask('\nIs this OK? (yes) ');
55
- if (confirm === 'no' || confirm === 'n') {
56
- logger.warn('Aborted.');
57
- return;
85
+ /**
86
+ * Internal helper to prompt for multiple fields sequentially via terminal.
87
+ *
88
+ * @param {Object[]} fields - Field definitions to prompt for
89
+ * @returns {Promise<Object>} Map of field keys to user answers
90
+ * @private
91
+ */
92
+ _prompt(fields) {
93
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
94
+ const answers = {};
95
+ return new Promise(resolve => {
96
+ let i = 0;
97
+ const next = () => {
98
+ if (i >= fields.length) {
99
+ rl.close();
100
+ resolve(answers);
101
+ return;
102
+ }
103
+ const f = fields[i++];
104
+ const label = this.logger.c.cyan(f.label) + (f.default ? this.logger.c.gray(` (${f.default})`) : '') + ': ';
105
+ rl.question(label, (val) => {
106
+ answers[f.key] = val.trim() || f.default || '';
107
+ next();
108
+ });
109
+ };
110
+ next();
111
+ });
58
112
  }
59
113
 
60
- for (const [k, v] of Object.entries(data)) pkg.setField(k, v);
61
- pkg.save();
62
- logger.success(`\nWrote to ${pkgFile}`);
63
- };
64
-
65
- function prompt(fields) {
66
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
- const answers = {};
68
- return new Promise(resolve => {
69
- let i = 0;
70
- function next() {
71
- if (i >= fields.length) { rl.close(); resolve(answers); return; }
72
- const f = fields[i++];
73
- const label = logger.c.cyan(f.label) + (f.default ? logger.c.gray(` (${f.default})`) : '') + ': ';
74
- rl.question(label, (val) => {
75
- answers[f.key] = val.trim() || f.default || '';
76
- next();
114
+ /**
115
+ * Internal helper to ask a single question via terminal.
116
+ *
117
+ * @param {string} label - Question text
118
+ * @returns {Promise<string>} Trimmed user response
119
+ * @private
120
+ */
121
+ _ask(label) {
122
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
123
+ return new Promise(resolve => {
124
+ rl.question(this.logger.c.cyan(label), (val) => {
125
+ rl.close();
126
+ resolve(val.trim());
77
127
  });
78
- }
79
- next();
80
- });
128
+ });
129
+ }
81
130
  }
82
131
 
83
- function ask(label) {
84
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
85
- return new Promise(resolve => {
86
- rl.question(logger.c.cyan(label), (val) => { rl.close(); resolve(val.trim()); });
87
- });
88
- }
132
+ module.exports = InitCommand;
133
+
@@ -1,139 +1,148 @@
1
1
  'use strict';
2
2
 
3
+ const BaseCommand = require('./base-command');
3
4
  const Resolver = require('../core/resolver');
4
5
  const Installer = require('../core/installer');
5
6
  const Lockfile = require('../core/lockfile');
6
7
  const PackageJSON = require('../core/package-json');
7
- const registry = require('../core/registry');
8
- const semver = require('../utils/semver');
9
8
  const { Spinner } = require('../utils/progress');
10
- const logger = require('../utils/logger');
11
- const config = require('../utils/config');
12
9
 
13
10
  /**
14
- * Command handler for 'jpm install' (and its aliases 'get', 'add', 'syn').
15
- * Orchestrates package resolution, downloading, and filesystem installation.
16
- *
17
- * @param {string[]} args - Positional arguments (package names/versions)
18
- * @param {Object} flags - CLI flags (e.g., --save-dev, --fast)
19
- * @returns {Promise<void>}
11
+ * InstallCommand handles the 'jpm install' command and its aliases.
12
+ * It coordinates package resolution, downloading, and filesystem installation.
20
13
  */
21
- module.exports = async function install(args, flags) {
22
- const cwd = process.cwd();
23
- const pkgJson = PackageJSON.fromDir(cwd);
24
- const lockfile = new Lockfile(cwd);
25
- const isDev = flags.D || flags['save-dev'];
26
- const isExact = flags.E || flags['save-exact'] || config.saveExact;
27
- const noSave = flags['no-save'];
28
- const dryRun = flags['dry-run'];
29
-
30
- // ── Parse packages to install ──────────────────────────────────────────────
31
- let toInstall = [];
32
-
33
- if (args.length) {
34
- // `jpm install express lodash@4.17.21 @types/node`
35
- for (const arg of args) {
36
- const lastAt = arg.lastIndexOf('@');
37
- const hasVersion = lastAt > 0;
38
- const name = hasVersion ? arg.slice(0, lastAt) : arg;
39
- const version = hasVersion ? arg.slice(lastAt + 1) : 'latest';
40
- toInstall.push({ name, version });
14
+ class InstallCommand extends BaseCommand {
15
+ constructor() {
16
+ super('install');
17
+ }
18
+
19
+ /**
20
+ * Executes the installation process.
21
+ *
22
+ * @param {string[]} args - Positional arguments (package names/versions)
23
+ * @param {Object} flags - CLI flags (e.g., --save-dev, --fast)
24
+ * @returns {Promise<void>}
25
+ */
26
+ async run(args, flags) {
27
+ const cwd = process.cwd();
28
+ const pkgJson = PackageJSON.fromDir(cwd);
29
+ const lockfile = new Lockfile(cwd);
30
+ const isDev = flags.D || flags['save-dev'];
31
+ const isExact = flags.E || flags['save-exact'] || this.config.saveExact;
32
+ const noSave = flags['no-save'];
33
+ const dryRun = flags['dry-run'];
34
+
35
+ // ── Parse packages to install ──────────────────────────────────────────────
36
+ let toInstall = [];
37
+
38
+ if (args.length) {
39
+ // Specific packages requested: `jpm install express lodash@4.17.21`
40
+ for (const arg of args) {
41
+ const lastAt = arg.lastIndexOf('@');
42
+ const hasVersion = lastAt > 0;
43
+ const name = hasVersion ? arg.slice(0, lastAt) : arg;
44
+ const version = hasVersion ? arg.slice(lastAt + 1) : 'latest';
45
+ toInstall.push({ name, version });
46
+ }
47
+ } else {
48
+ // General install from package.json
49
+ const spinner = new Spinner('Reading package.json...').start();
50
+ const allDeps = {
51
+ ...pkgJson.dependencies,
52
+ ...(flags.production ? {} : pkgJson.devDependencies),
53
+ };
54
+ spinner.succeed(`Found ${Object.keys(allDeps).length} dependencies`);
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`);
66
+ return;
67
+ }
68
+
69
+ toInstall = Object.entries(allDeps).map(([name, version]) => ({ name, version }));
41
70
  }
42
- } else {
43
- const spinner = new Spinner('Reading package.json...').start();
44
- const allDeps = {
45
- ...pkgJson.dependencies,
46
- ...(flags.production ? {} : pkgJson.devDependencies),
47
- };
48
- spinner.succeed(`Found ${Object.keys(allDeps).length} dependencies`);
49
-
50
- // Prioritize lockfile-based installation for deterministic results
51
- if (lockfile.exists()) {
52
- logger.info('Using lock file for deterministic install');
53
- const lockData = lockfile.allPackages();
54
- const installer = new Installer(cwd);
55
- const bar = new (require('../utils/progress').Spinner)('Installing from lock file...');
56
- bar.start();
57
- const fakeMap = new Map(lockData.map(p => [`${p.name}@${p.version}`, p]));
58
- await installer.installAll(fakeMap, { dryRun, flags });
59
- bar.succeed(`Installed ${lockData.length} packages`);
71
+
72
+ if (!toInstall.length) {
73
+ this.logger.info('Nothing to install.');
60
74
  return;
61
75
  }
62
76
 
63
- toInstall = Object.entries(allDeps).map(([name, version]) => ({ name, version }));
64
- }
77
+ // ── Resolve ────────────────────────────────────────────────────────────────
78
+ const resolveSpinner = new Spinner(`Resolving ${toInstall.length} package(s)...`).start();
79
+ const resolver = new Resolver();
65
80
 
66
- if (!toInstall.length) {
67
- logger.info('Nothing to install.');
68
- return;
69
- }
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
+ }
70
87
 
71
- // ── Resolve ────────────────────────────────────────────────────────────────
72
- const resolveSpinner = new Spinner(`Resolving ${toInstall.length} package(s)...`).start();
73
- const resolver = new Resolver();
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
+ }
74
98
 
75
- const deps = {};
76
- const devDeps = {};
77
- for (const { name, version } of toInstall) {
78
- if (isDev) devDeps[name] = version;
79
- else deps[name] = version;
80
- }
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
+ }
81
104
 
82
- let resolvedMap;
83
- try {
84
- resolvedMap = await resolver.resolve(deps, devDeps, {}, (count, total) => {
85
- resolveSpinner.text = `Resolving packages... (${count} resolved)`;
86
- });
87
- resolveSpinner.succeed(`Resolved ${resolvedMap.size} packages (including transitive deps)`);
88
- } catch (err) {
89
- resolveSpinner.fail(`Resolution failed: ${err.message}`);
90
- process.exit(1);
91
- }
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
+ }
92
114
 
93
- // Warn about circular deps
94
- const circular = resolver.findCircular();
95
- if (circular.length) {
96
- logger.warn(`Circular dependencies detected:\n ${circular.join('\n ')}`);
97
- }
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
+ }
98
125
 
99
- // ── Install ────────────────────────────────────────────────────────────────
100
- logger.info(`Installing ${resolvedMap.size} packages...`);
101
- const installer = new Installer(cwd);
102
- try {
103
- await installer.installAll(resolvedMap, { dryRun, flags });
104
- } catch (err) {
105
- logger.error(`Install failed: ${err.message}`);
106
- process.exit(1);
107
- }
126
+ if (!dryRun) {
127
+ lockfile.update(resolvedMap).save();
128
+ this.logger.verbose('Updated jpm-lock.json');
129
+ }
108
130
 
109
- // ── Update package.json & lock file ─────────────────────────────────────────
110
- if (!noSave && !dryRun && args.length) {
111
- for (const { name } of toInstall) {
112
- // Find actual resolved version
113
- const resolved = [...resolvedMap.values()].find(m => m.name === name);
114
- if (!resolved) continue;
115
- pkgJson.addDependency(name, resolved.version, { dev: !!isDev, exact: isExact });
131
+ // ── Summary ────────────────────────────────────────────────────────────────
132
+ const dupes = resolver.deduplicate();
133
+ if (dupes.length) {
134
+ this.logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
116
135
  }
117
- pkgJson.save();
118
- logger.verbose('Updated package.json');
119
- }
120
136
 
121
- if (!dryRun) {
122
- lockfile.update(resolvedMap).save();
123
- logger.verbose('Updated jpm-lock.json');
124
- }
137
+ const directly = toInstall.map(({ name }) => {
138
+ const r = [...resolvedMap.values()].find(m => m.name === name);
139
+ return r ? `${r.name}@${r.version}` : name;
140
+ });
125
141
 
126
- // ── Summary ────────────────────────────────────────────────────────────────
127
- const dupes = resolver.deduplicate();
128
- if (dupes.length) {
129
- logger.verbose(`Deduplication: ${dupes.length} packages have multiple versions`);
142
+ this.logger.success(`\nadded ${resolvedMap.size} packages`);
143
+ directly.forEach(p => this.logger.log(` + ${this.logger.c.green(p)}`));
130
144
  }
145
+ }
131
146
 
132
- const directly = toInstall.map(({ name }) => {
133
- const r = [...resolvedMap.values()].find(m => m.name === name);
134
- return r ? `${r.name}@${r.version}` : name;
135
- });
147
+ module.exports = InstallCommand;
136
148
 
137
- logger.success(`\nadded ${resolvedMap.size} packages`);
138
- directly.forEach(p => logger.log(` + ${logger.c.green(p)}`));
139
- };