offline-npm-manager 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/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # 📦 Offline NPM Manager - CLI Tool
2
+
3
+ A command-line tool for downloading npm packages when online and installing them offline later.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ **✅ Add Packages** - Download packages and dependencies for offline use
10
+
11
+ **✅ Install Packages** - Install from local cache without internet
12
+
13
+ **✅ List Packages** - View all cached packages
14
+
15
+ **✅ Remove Packages** - Clean up cached packages
16
+
17
+ **✅ Dependency Management** - Automatically cache package dependencies
18
+
19
+ **✅ Smart Caching** - Detect already-cached packages
20
+
21
+ **✅ Scoped Packages** - Full support for `@scope/package` naming
22
+
23
+ **✅ Cross-Platform** - Works on Windows, macOS, and Linux
24
+
25
+ **✅ Version Control** - Store and manage multiple versions
26
+
27
+ **✅ Real-time Feedback** - Progress indicators and error messages
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ### Prerequisites
34
+
35
+ - **Node.js** 16 or higher
36
+ - **npm** 8 or higher
37
+
38
+ ### Install Globally
39
+
40
+ ```bash
41
+ npm install -g offline-npm-manager
42
+ ```
43
+
44
+ This installs the `offline-npm` command globally.
45
+
46
+ ### Offline Installation
47
+
48
+ ```bash
49
+ npm install -g offline-npm-manager --offline
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Usage
55
+
56
+ ### Add a Package
57
+
58
+ Download and store a package locally (requires internet):
59
+
60
+ ```bash
61
+ offline-npm add <package>
62
+ ```
63
+
64
+ Examples:
65
+
66
+ ```bash
67
+ offline-npm add axios
68
+ offline-npm add react --deps
69
+ offline-npm add react@17.0.2
70
+ offline-npm add @babel/core
71
+ ```
72
+
73
+ **Options:**
74
+
75
+ - `-d, --deps` → Download all dependencies recursively
76
+ - `-s, --storage <path>` → Custom storage directory
77
+
78
+ ---
79
+
80
+ ### Install a Package
81
+
82
+ Install from local cache (works offline):
83
+
84
+ ```bash
85
+ offline-npm install <package>
86
+ ```
87
+
88
+ Examples:
89
+
90
+ ```bash
91
+ offline-npm install axios --save
92
+ offline-npm install lodash --save-dev
93
+ offline-npm install react@17.0.2
94
+ ```
95
+
96
+ **Options:**
97
+
98
+ - `--save` → Add to dependencies
99
+ - `--save-dev` → Add to devDependencies
100
+ - `-s, --storage <path>` → Custom storage
101
+
102
+ ---
103
+
104
+ ### List Cached Packages
105
+
106
+ ```bash
107
+ offline-npm list
108
+ offline-npm ls
109
+ ```
110
+
111
+ Example output:
112
+
113
+ ```
114
+ 📦 offline-npm list
115
+
116
+ Storage: ~/.offline-npm-cache
117
+
118
+ ┌──────────┬─────────┬──────────┬───────────────────────┬─────────┐
119
+ │ Package │ Version │ Size │ Downloaded │ Status │
120
+ ├──────────┼─────────┼──────────┼───────────────────────┼─────────┤
121
+ │ lodash │ 4.17.23 │ 307.5 KB │ 3/24/2026, 9:48 AM │ ✔ ready │
122
+ │ express │ 5.2.1 │ 22.6 KB │ 3/24/2026, 10:15 AM │ ✔ ready │
123
+ └──────────┴─────────┴──────────┴───────────────────────┴─────────┘
124
+ ```
125
+
126
+ ---
127
+
128
+ ### Remove a Package
129
+
130
+ ```bash
131
+ offline-npm remove <package>
132
+ ```
133
+
134
+ Example:
135
+
136
+ ```bash
137
+ offline-npm remove lodash
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Storage
143
+
144
+ Default locations:
145
+
146
+ - **Windows** → `%USERPROFILE%\.offline-npm-cache`
147
+ - **macOS/Linux** → `~/.offline-npm-cache`
148
+
149
+ Override storage:
150
+
151
+ ```bash
152
+ offline-npm add react --storage /custom/path
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Storage Structure
158
+
159
+ ```
160
+ ~/.offline-npm-cache/
161
+ ├── lodash/
162
+ │ └── 4.17.23/
163
+ │ ├── lodash-4.17.23.tgz
164
+ │ └── meta.json
165
+ ├── express/
166
+ │ └── 5.2.1/
167
+ │ ├── express-5.2.1.tgz
168
+ │ └── meta.json
169
+ └── @babel/
170
+ └── core/
171
+ └── 7.20.5/
172
+ ├── core-7.20.5.tgz
173
+ └── meta.json
174
+ ```
175
+
176
+ ---
177
+
178
+ ## meta.json Example
179
+
180
+ ```json
181
+ {
182
+ "name": "express",
183
+ "version": "5.2.1",
184
+ "size": 23150,
185
+ "downloadedAt": "2026-03-24T10:15:30Z",
186
+ "hasDeps": true
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Uninstall
193
+
194
+ ```bash
195
+ npm uninstall -g offline-npm-manager
196
+ ```
197
+
198
+ This removes the CLI.
199
+
200
+ (Optional: manually delete cache if needed)
201
+
202
+ ```bash
203
+ rm -rf ~/.offline-npm-cache
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Example Workflow
209
+
210
+ ```bash
211
+ # Step 1: Online
212
+ offline-npm add express@4.18.2 --deps
213
+ offline-npm add lodash
214
+ offline-npm list
215
+
216
+ # Step 2: Go Offline
217
+
218
+ # Step 3: Install
219
+ mkdir my-project && cd my-project
220
+ npm init -y
221
+
222
+ offline-npm install express@4.18.2 --save
223
+ offline-npm install lodash --save
224
+ ```
225
+
226
+ ---
227
+
228
+ ## How It Works
229
+
230
+ | Operation | Description |
231
+ | ------------ | ---------------------------------- |
232
+ | Add Package | Uses `npm pack` to download `.tgz` |
233
+ | Install | Installs from local `.tgz` |
234
+ | List | Reads cached metadata |
235
+ | Remove | Deletes cached files |
236
+ | Dependencies | Recursively cached with `--deps` |
237
+
238
+ ---
239
+
240
+ ## Requirements
241
+
242
+ - Node.js ≥ 16
243
+ - npm ≥ 8
244
+
245
+ ---
246
+
247
+ ## Troubleshooting
248
+
249
+ ### npm not found
250
+
251
+ ```bash
252
+ npm --version
253
+ ```
254
+
255
+ Install Node.js or fix PATH.
256
+
257
+ ---
258
+
259
+ ### Package not found
260
+
261
+ ```bash
262
+ offline-npm add lodash
263
+ ```
264
+
265
+ Check spelling or internet connection.
266
+
267
+ ---
268
+
269
+ ### Missing dependencies
270
+
271
+ ```bash
272
+ offline-npm add express --deps
273
+ ```
274
+
275
+ ---
276
+
277
+ ## License
278
+
279
+ MIT
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { program } = require("commander");
6
+ const { addPackage } = require("../src/add.js");
7
+ const { installPackage } = require("../src/install.js");
8
+ const { listPackages } = require("../src/list.js");
9
+ const { removePackage } = require("../src/remove.js");
10
+ const { version } = require("../package.json");
11
+
12
+ program
13
+ .name("offline-npm")
14
+ .description(
15
+ "📦 Download npm packages when online, install them offline later",
16
+ )
17
+ .version(version);
18
+
19
+ program
20
+ .command("add <package>")
21
+ .description("Download a package and store it locally (requires internet)")
22
+ .option("-d, --deps", "Also download all dependencies recursively", false)
23
+ .option("-s, --storage <path>", "Custom storage directory")
24
+ .action(async (pkg, options) => {
25
+ await addPackage(pkg, options);
26
+ });
27
+
28
+ program
29
+ .command("install <package>")
30
+ .description("Install a package from local offline storage")
31
+ .option("-s, --storage <path>", "Custom storage directory")
32
+ .option("--save", "Add to package.json dependencies", false)
33
+ .option("--save-dev", "Add to package.json devDependencies", false)
34
+ .action(async (pkg, options) => {
35
+ await installPackage(pkg, options);
36
+ });
37
+
38
+ program
39
+ .command("list")
40
+ .alias("ls")
41
+ .description("List all locally stored packages")
42
+ .option("-s, --storage <path>", "Custom storage directory")
43
+ .action(async (options) => {
44
+ await listPackages(options);
45
+ });
46
+
47
+ program
48
+ .command("remove <package>")
49
+ .alias("rm")
50
+ .description("Remove a package from local offline storage")
51
+ .option("-s, --storage <path>", "Custom storage directory")
52
+ .action(async (pkg, options) => {
53
+ await removePackage(pkg, options);
54
+ });
55
+
56
+ program.parse(process.argv);
57
+
58
+ if (!process.argv.slice(2).length) {
59
+ program.outputHelp();
60
+ }
package/dist/cli.cjs ADDED
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __commonJS = (cb, mod) => function __require() {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ };
7
+
8
+ // src/parser.js
9
+ var require_parser = __commonJS({
10
+ "src/parser.js"(exports2, module2) {
11
+ "use strict";
12
+ function parsePackage(input) {
13
+ input = input.trim();
14
+ if (input.startsWith("@")) {
15
+ const secondAt = input.indexOf("@", 1);
16
+ if (secondAt === -1) {
17
+ return { name: input, version: "latest", raw: input };
18
+ }
19
+ return {
20
+ name: input.slice(0, secondAt),
21
+ version: input.slice(secondAt + 1),
22
+ raw: input
23
+ };
24
+ }
25
+ const atIndex = input.indexOf("@");
26
+ if (atIndex === -1) {
27
+ return { name: input, version: "latest", raw: input };
28
+ }
29
+ return {
30
+ name: input.slice(0, atIndex),
31
+ version: input.slice(atIndex + 1),
32
+ raw: input
33
+ };
34
+ }
35
+ function packageLabel(name, version2) {
36
+ return `${name}@${version2}`;
37
+ }
38
+ module2.exports = { parsePackage, packageLabel };
39
+ }
40
+ });
41
+
42
+ // src/storage.js
43
+ var require_storage = __commonJS({
44
+ "src/storage.js"(exports2, module2) {
45
+ "use strict";
46
+ var path = require("path");
47
+ var os = require("os");
48
+ var fs = require("fs");
49
+ var DEFAULT_STORAGE = path.join(os.homedir(), ".offline-npm-cache");
50
+ function getStorageDir(customPath = null) {
51
+ const dir = customPath ? path.resolve(customPath) : DEFAULT_STORAGE;
52
+ ensureDir(dir);
53
+ return dir;
54
+ }
55
+ function getPackageDir(storageDir, name, version2) {
56
+ const dir = path.join(storageDir, name, version2);
57
+ ensureDir(dir);
58
+ return dir;
59
+ }
60
+ function ensureDir(dir) {
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+ }
65
+ function readMeta(pkgDir) {
66
+ const metaPath = path.join(pkgDir, "meta.json");
67
+ if (!fs.existsSync(metaPath))
68
+ return null;
69
+ return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
70
+ }
71
+ function writeMeta(pkgDir, meta) {
72
+ const metaPath = path.join(pkgDir, "meta.json");
73
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
74
+ }
75
+ module2.exports = {
76
+ DEFAULT_STORAGE,
77
+ getStorageDir,
78
+ getPackageDir,
79
+ ensureDir,
80
+ readMeta,
81
+ writeMeta
82
+ };
83
+ }
84
+ });
85
+
86
+ // src/logger.js
87
+ var require_logger = __commonJS({
88
+ "src/logger.js"(exports2, module2) {
89
+ "use strict";
90
+ var RESET = "\x1B[0m";
91
+ var BOLD = "\x1B[1m";
92
+ var DIM = "\x1B[2m";
93
+ var GREEN = "\x1B[32m";
94
+ var CYAN = "\x1B[36m";
95
+ var YELLOW = "\x1B[33m";
96
+ var RED = "\x1B[31m";
97
+ var BLUE = "\x1B[34m";
98
+ function info(msg) {
99
+ console.log(`${CYAN}\u2139${RESET} ${msg}`);
100
+ }
101
+ function success(msg) {
102
+ console.log(`${GREEN}\u2714${RESET} ${msg}`);
103
+ }
104
+ function warn(msg) {
105
+ console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
106
+ }
107
+ function error(msg) {
108
+ console.error(`${RED}\u2716${RESET} ${msg}`);
109
+ }
110
+ function step(msg) {
111
+ console.log(`${BLUE}\u203A${RESET} ${msg}`);
112
+ }
113
+ function dim(msg) {
114
+ console.log(`${DIM} ${msg}${RESET}`);
115
+ }
116
+ function bold(msg) {
117
+ return `${BOLD}${msg}${RESET}`;
118
+ }
119
+ function header(msg) {
120
+ console.log(`
121
+ ${BOLD}${CYAN}${msg}${RESET}
122
+ `);
123
+ }
124
+ function table(rows) {
125
+ if (!rows.length)
126
+ return;
127
+ const cols = Object.keys(rows[0]);
128
+ const widths = cols.map(
129
+ (c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
130
+ );
131
+ const line = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
132
+ const header2 = cols.map((c, i) => ` ${BOLD}${c.padEnd(widths[i])}${RESET} `).join("\u2502");
133
+ console.log("\u250C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510");
134
+ console.log("\u2502" + header2 + "\u2502");
135
+ console.log("\u251C" + line + "\u2524");
136
+ rows.forEach((row) => {
137
+ const cells = cols.map((c, i) => ` ${String(row[c] ?? "").padEnd(widths[i])} `).join("\u2502");
138
+ console.log("\u2502" + cells + "\u2502");
139
+ });
140
+ console.log("\u2514" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518");
141
+ }
142
+ module2.exports = { info, success, warn, error, step, dim, bold, header, table };
143
+ }
144
+ });
145
+
146
+ // src/add.js
147
+ var require_add = __commonJS({
148
+ "src/add.js"(exports2, module2) {
149
+ "use strict";
150
+ var { execSync, spawnSync } = require("child_process");
151
+ var fs = require("fs");
152
+ var path = require("path");
153
+ var { parsePackage, packageLabel } = require_parser();
154
+ var { getStorageDir, getPackageDir, writeMeta } = require_storage();
155
+ var log = require_logger();
156
+ function resolveVersion(name, version2) {
157
+ try {
158
+ const result = execSync(
159
+ `npm view ${packageLabel(name, version2)} version --json`,
160
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
161
+ ).trim();
162
+ return result.replace(/"/g, "");
163
+ } catch (err) {
164
+ throw new Error(
165
+ `Could not resolve version for ${packageLabel(name, version2)}. Are you online? Does this package exist?`
166
+ );
167
+ }
168
+ }
169
+ function getDependencies(name, version2) {
170
+ try {
171
+ const raw = execSync(
172
+ `npm view ${packageLabel(name, version2)} dependencies --json`,
173
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
174
+ ).trim();
175
+ if (!raw || raw === "undefined")
176
+ return {};
177
+ return JSON.parse(raw);
178
+ } catch {
179
+ return {};
180
+ }
181
+ }
182
+ function npmPack(pkgLabel, destDir) {
183
+ if (!fs.existsSync(destDir)) {
184
+ fs.mkdirSync(destDir, { recursive: true });
185
+ }
186
+ try {
187
+ const cmd = `npm pack ${pkgLabel} --pack-destination "${destDir}"`;
188
+ const output = execSync(cmd, { encoding: "utf-8" });
189
+ const lines = output.trim().split("\n").filter((l) => l.trim() && !l.includes("npm"));
190
+ const tgzName = lines.pop().trim();
191
+ if (!tgzName) {
192
+ throw new Error(`No output from npm pack for ${pkgLabel}`);
193
+ }
194
+ return path.join(destDir, tgzName);
195
+ } catch (err) {
196
+ log.error(`npm pack failed: ${err.message}`);
197
+ throw err;
198
+ }
199
+ }
200
+ async function downloadPackage(name, version2, storageDir, downloadDeps, visited = /* @__PURE__ */ new Set()) {
201
+ const label = packageLabel(name, version2);
202
+ if (visited.has(label))
203
+ return;
204
+ visited.add(label);
205
+ log.step(`Resolving ${log.bold(label)} ...`);
206
+ let resolvedVersion;
207
+ try {
208
+ resolvedVersion = resolveVersion(name, version2);
209
+ } catch (err) {
210
+ log.error(err.message);
211
+ process.exit(1);
212
+ }
213
+ const pkgDir = getPackageDir(storageDir, name, resolvedVersion);
214
+ const metaPath = path.join(pkgDir, "meta.json");
215
+ if (fs.existsSync(metaPath)) {
216
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
217
+ if (meta.tgz && fs.existsSync(path.join(pkgDir, meta.tgz))) {
218
+ log.info(
219
+ `Already cached: ${log.bold(packageLabel(name, resolvedVersion))}`
220
+ );
221
+ return;
222
+ }
223
+ }
224
+ log.step(`Downloading ${log.bold(packageLabel(name, resolvedVersion))} ...`);
225
+ let tgzPath;
226
+ try {
227
+ tgzPath = npmPack(packageLabel(name, resolvedVersion), pkgDir);
228
+ } catch (err) {
229
+ log.error(`Failed to download ${label}: ${err.message}`);
230
+ process.exit(1);
231
+ }
232
+ const tgzName = path.basename(tgzPath);
233
+ const stats = fs.statSync(tgzPath);
234
+ writeMeta(pkgDir, {
235
+ name,
236
+ version: resolvedVersion,
237
+ requestedVersion: version2,
238
+ tgz: tgzName,
239
+ size: stats.size,
240
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
241
+ hasDeps: downloadDeps
242
+ });
243
+ log.success(
244
+ `Saved ${log.bold(packageLabel(name, resolvedVersion))} (${(stats.size / 1024).toFixed(1)} KB) \u2192 ${pkgDir}`
245
+ );
246
+ if (downloadDeps) {
247
+ const deps = getDependencies(name, resolvedVersion);
248
+ const depEntries = Object.entries(deps);
249
+ if (depEntries.length > 0) {
250
+ log.info(
251
+ `Found ${depEntries.length} dependenc${depEntries.length === 1 ? "y" : "ies"} for ${packageLabel(name, resolvedVersion)}`
252
+ );
253
+ for (const [depName, depRange] of depEntries) {
254
+ await downloadPackage(
255
+ depName,
256
+ depRange,
257
+ storageDir,
258
+ downloadDeps,
259
+ visited
260
+ );
261
+ }
262
+ }
263
+ }
264
+ }
265
+ async function addPackage2(pkgInput, options) {
266
+ const { name, version: version2 } = parsePackage(pkgInput);
267
+ const storageDir = getStorageDir(options.storage);
268
+ log.header(`\u{1F4E6} offline-npm add`);
269
+ log.info(`Storage directory: ${storageDir}`);
270
+ if (options.deps) {
271
+ log.info(`Dependency download: ${log.bold("enabled")}`);
272
+ }
273
+ console.log("");
274
+ await downloadPackage(name, version2, storageDir, options.deps);
275
+ console.log("");
276
+ log.success(
277
+ `Done! Run ${log.bold(`offline-npm install ${pkgInput}`)} anytime \u2014 even offline.`
278
+ );
279
+ }
280
+ module2.exports = { addPackage: addPackage2 };
281
+ }
282
+ });
283
+
284
+ // src/install.js
285
+ var require_install = __commonJS({
286
+ "src/install.js"(exports2, module2) {
287
+ "use strict";
288
+ var { spawnSync } = require("child_process");
289
+ var fs = require("fs");
290
+ var path = require("path");
291
+ var { parsePackage, packageLabel } = require_parser();
292
+ var { getStorageDir, readMeta } = require_storage();
293
+ var log = require_logger();
294
+ function findCachedVersions(storageDir, name) {
295
+ const pkgPath = path.join(storageDir, ...name.split("/"));
296
+ if (!fs.existsSync(pkgPath))
297
+ return [];
298
+ return fs.readdirSync(pkgPath).filter((v) => {
299
+ const meta = readMeta(path.join(pkgPath, v));
300
+ return meta !== null;
301
+ }).map((v) => ({
302
+ version: v,
303
+ pkgDir: path.join(pkgPath, v),
304
+ meta: readMeta(path.join(pkgPath, v))
305
+ }));
306
+ }
307
+ function pickVersion(cached, requestedVersion) {
308
+ if (!cached.length)
309
+ return null;
310
+ if (requestedVersion === "latest") {
311
+ return cached.sort(
312
+ (a, b) => new Date(b.meta.downloadedAt) - new Date(a.meta.downloadedAt)
313
+ )[0];
314
+ }
315
+ return cached.find((c) => c.version === requestedVersion) || null;
316
+ }
317
+ function runNpmInstall(tgzPath, saveFlag) {
318
+ const args = [
319
+ "install",
320
+ tgzPath,
321
+ "--prefer-offline",
322
+ // Use cache first before fetching
323
+ "--no-audit"
324
+ // Skip npm audit (requires network)
325
+ ];
326
+ if (saveFlag === "save")
327
+ args.push("--save");
328
+ if (saveFlag === "save-dev")
329
+ args.push("--save-dev");
330
+ const result = spawnSync("npm", args, {
331
+ encoding: "utf-8",
332
+ stdio: "inherit",
333
+ shell: true
334
+ // Windows compatibility
335
+ });
336
+ return result.status === 0;
337
+ }
338
+ function normalizeResolvedUrl(fileUri) {
339
+ if (typeof fileUri !== "string" || !fileUri.startsWith("file:"))
340
+ return null;
341
+ let absolute = fileUri.replace(/^file:\/\//, "").replace(/^file:/, "");
342
+ absolute = decodeURIComponent(absolute).replace(/\\/g, "/");
343
+ const marker = ".offline-npm-cache/";
344
+ const idx = absolute.indexOf(marker);
345
+ if (idx === -1)
346
+ return null;
347
+ const subpath = absolute.slice(idx + marker.length);
348
+ const parts = subpath.split("/").filter(Boolean);
349
+ let packageName;
350
+ let version2;
351
+ if (parts[0].startsWith("@")) {
352
+ if (parts.length < 4)
353
+ return null;
354
+ packageName = `${parts[0]}/${parts[1]}`;
355
+ version2 = parts[2];
356
+ } else {
357
+ if (parts.length < 3)
358
+ return null;
359
+ packageName = parts[0];
360
+ version2 = parts[1];
361
+ }
362
+ const baseName = packageName.includes("/") ? packageName.split("/").pop() : packageName;
363
+ const tarballName = `${baseName}-${version2}.tgz`;
364
+ return `https://registry.npmjs.org/${packageName}/-/${tarballName}`;
365
+ }
366
+ function sanitizePackageLock() {
367
+ const lockFile = path.resolve("package-lock.json");
368
+ if (!fs.existsSync(lockFile))
369
+ return;
370
+ try {
371
+ let sanitizeNode = function(node) {
372
+ if (!node || typeof node !== "object")
373
+ return;
374
+ if (node.resolved && typeof node.resolved === "string") {
375
+ const normalized = normalizeResolvedUrl(node.resolved);
376
+ if (normalized)
377
+ node.resolved = normalized;
378
+ }
379
+ if (node.dependencies && typeof node.dependencies === "object") {
380
+ for (const depName of Object.keys(node.dependencies)) {
381
+ sanitizeNode(node.dependencies[depName]);
382
+ }
383
+ }
384
+ };
385
+ const lockData = JSON.parse(fs.readFileSync(lockFile, "utf-8"));
386
+ if (lockData.packages && typeof lockData.packages === "object") {
387
+ for (const pkgPath of Object.keys(lockData.packages)) {
388
+ sanitizeNode(lockData.packages[pkgPath]);
389
+ }
390
+ }
391
+ if (lockData.dependencies && typeof lockData.dependencies === "object") {
392
+ sanitizeNode(lockData.dependencies);
393
+ }
394
+ fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2), "utf-8");
395
+ } catch (err) {
396
+ log.warn(`Could not sanitize package-lock.json: ${err.message}`);
397
+ }
398
+ }
399
+ function sanitizePackageJson(name, version2, saveFlag) {
400
+ if (!saveFlag)
401
+ return;
402
+ const pkgJsonPath = path.resolve("package.json");
403
+ if (!fs.existsSync(pkgJsonPath))
404
+ return;
405
+ try {
406
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
407
+ const section = saveFlag === "save-dev" ? "devDependencies" : "dependencies";
408
+ if (!pkgJson[section])
409
+ pkgJson[section] = {};
410
+ const current = pkgJson[section][name];
411
+ if (typeof current === "string" && current.startsWith("file:")) {
412
+ pkgJson[section][name] = version2;
413
+ }
414
+ console.log({ pkgJson });
415
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
416
+ } catch (err) {
417
+ log.warn(`Could not sanitize package.json (${name}): ${err.message}`);
418
+ }
419
+ }
420
+ async function installPackage2(pkgInput, options) {
421
+ const { name, version: version2 } = parsePackage(pkgInput);
422
+ const storageDir = getStorageDir(options.storage);
423
+ log.header(`\u{1F4E6} offline-npm install`);
424
+ log.info(
425
+ `Looking up ${log.bold(packageLabel(name, version2))} in local cache...`
426
+ );
427
+ log.dim(`Storage: ${storageDir}`);
428
+ console.log("");
429
+ const cached = findCachedVersions(storageDir, name);
430
+ if (!cached.length) {
431
+ log.error(`Package ${log.bold(name)} not found in local cache.`);
432
+ log.info(
433
+ `Run ${log.bold(`offline-npm add ${pkgInput}`)} while online first.`
434
+ );
435
+ process.exit(1);
436
+ }
437
+ const match = pickVersion(cached, version2);
438
+ if (!match) {
439
+ log.error(
440
+ `Version ${log.bold(version2)} of ${log.bold(name)} is not cached.`
441
+ );
442
+ log.info(`Cached versions: ${cached.map((c) => c.version).join(", ")}`);
443
+ log.info(
444
+ `Run ${log.bold(`offline-npm add ${pkgInput}`)} while online to download it.`
445
+ );
446
+ process.exit(1);
447
+ }
448
+ const tgzPath = path.join(match.pkgDir, match.meta.tgz);
449
+ if (!fs.existsSync(tgzPath)) {
450
+ log.error(`Cached .tgz file is missing: ${tgzPath}`);
451
+ log.info(
452
+ `Run ${log.bold(`offline-npm add ${pkgInput}`)} again to re-download.`
453
+ );
454
+ process.exit(1);
455
+ }
456
+ log.step(
457
+ `Installing ${log.bold(packageLabel(name, match.version))} from cache...`
458
+ );
459
+ log.dim(`Source: ${tgzPath}`);
460
+ console.log("");
461
+ const saveFlag = options.saveDev ? "save-dev" : options.save ? "save" : null;
462
+ const ok = runNpmInstall(tgzPath, saveFlag);
463
+ console.log("");
464
+ if (ok) {
465
+ log.success(
466
+ `Installed ${log.bold(packageLabel(name, match.version))} successfully (offline).`
467
+ );
468
+ sanitizePackageJson(name, match.version, saveFlag);
469
+ sanitizePackageLock();
470
+ log.info("Package metadata was rewritten to production-friendly sources.");
471
+ } else {
472
+ log.error(`npm install failed. Check output above for details.`);
473
+ process.exit(1);
474
+ }
475
+ }
476
+ module2.exports = { installPackage: installPackage2 };
477
+ }
478
+ });
479
+
480
+ // src/list.js
481
+ var require_list = __commonJS({
482
+ "src/list.js"(exports2, module2) {
483
+ "use strict";
484
+ var fs = require("fs");
485
+ var path = require("path");
486
+ var { getStorageDir, readMeta } = require_storage();
487
+ var log = require_logger();
488
+ function collectPackages(storageDir) {
489
+ const results = [];
490
+ function walk(dir, nameParts) {
491
+ if (!fs.existsSync(dir))
492
+ return;
493
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
494
+ for (const entry of entries) {
495
+ if (!entry.isDirectory())
496
+ continue;
497
+ const fullPath = path.join(dir, entry.name);
498
+ if (nameParts.length === 0 && entry.name.startsWith("@")) {
499
+ walk(fullPath, [entry.name]);
500
+ continue;
501
+ }
502
+ if (nameParts.length === 0 || nameParts.length === 1 && nameParts[0].startsWith("@")) {
503
+ const pkgName = nameParts.length ? `${nameParts[0]}/${entry.name}` : entry.name;
504
+ walkVersions(fullPath, pkgName);
505
+ }
506
+ }
507
+ }
508
+ function walkVersions(pkgDir, pkgName) {
509
+ if (!fs.existsSync(pkgDir))
510
+ return;
511
+ const entries = fs.readdirSync(pkgDir, { withFileTypes: true });
512
+ for (const entry of entries) {
513
+ if (!entry.isDirectory())
514
+ continue;
515
+ const versionDir = path.join(pkgDir, entry.name);
516
+ const meta = readMeta(versionDir);
517
+ if (meta) {
518
+ const tgzPath = path.join(versionDir, meta.tgz || "");
519
+ const tgzExists = fs.existsSync(tgzPath);
520
+ results.push({
521
+ id: `${pkgName}@${entry.name}`,
522
+ name: pkgName,
523
+ version: entry.name,
524
+ size: meta.size || 0,
525
+ sizeLabel: meta.size ? `${(meta.size / 1024).toFixed(1)} KB` : "?",
526
+ downloadedAt: meta.downloadedAt || null,
527
+ tgz: meta.tgz || null,
528
+ tgzPath,
529
+ status: tgzExists ? "ready" : "missing",
530
+ hasDeps: meta.hasDeps || false
531
+ });
532
+ }
533
+ }
534
+ }
535
+ walk(storageDir, []);
536
+ return results.sort(
537
+ (a, b) => new Date(b.downloadedAt) - new Date(a.downloadedAt)
538
+ );
539
+ }
540
+ async function listPackages2(options) {
541
+ const storageDir = getStorageDir(options.storage);
542
+ log.header("\u{1F4E6} offline-npm list");
543
+ log.dim(`Storage: ${storageDir}`);
544
+ console.log("");
545
+ const packages = collectPackages(storageDir);
546
+ if (!packages.length) {
547
+ log.warn("No packages cached yet.");
548
+ log.info(
549
+ `Run ${log.bold("offline-npm add <package>")} to download packages.`
550
+ );
551
+ return;
552
+ }
553
+ const displayTable = packages.map((p) => ({
554
+ Package: p.name,
555
+ Version: p.version,
556
+ Size: p.sizeLabel,
557
+ Downloaded: p.downloadedAt ? new Date(p.downloadedAt).toLocaleString() : "?",
558
+ Status: p.status === "ready" ? "\u2714 ready" : "\u2716 missing"
559
+ }));
560
+ log.table(displayTable);
561
+ console.log("");
562
+ log.success(
563
+ `${packages.length} package version${packages.length === 1 ? "" : "s"} in cache.`
564
+ );
565
+ }
566
+ module2.exports = { listPackages: listPackages2, collectPackages };
567
+ }
568
+ });
569
+
570
+ // src/remove.js
571
+ var require_remove = __commonJS({
572
+ "src/remove.js"(exports2, module2) {
573
+ "use strict";
574
+ var fs = require("fs");
575
+ var path = require("path");
576
+ var { parsePackage, packageLabel } = require_parser();
577
+ var { getStorageDir } = require_storage();
578
+ var log = require_logger();
579
+ function rmDir(dir) {
580
+ fs.rmSync(dir, { recursive: true, force: true });
581
+ }
582
+ async function removePackage2(pkgInput, options) {
583
+ const { name, version: version2 } = parsePackage(pkgInput);
584
+ const storageDir = getStorageDir(options.storage);
585
+ log.header("\u{1F4E6} offline-npm remove");
586
+ const nameParts = name.startsWith("@") ? name.split("/") : [name];
587
+ const pkgDir = path.join(storageDir, ...nameParts);
588
+ if (!fs.existsSync(pkgDir)) {
589
+ log.error(`Package ${log.bold(name)} is not in the local cache.`);
590
+ process.exit(1);
591
+ }
592
+ if (version2 === "latest") {
593
+ rmDir(pkgDir);
594
+ log.success(`Removed all cached versions of ${log.bold(name)}.`);
595
+ } else {
596
+ const versionDir = path.join(pkgDir, version2);
597
+ if (!fs.existsSync(versionDir)) {
598
+ log.error(
599
+ `Version ${log.bold(version2)} of ${log.bold(name)} is not cached.`
600
+ );
601
+ process.exit(1);
602
+ }
603
+ rmDir(versionDir);
604
+ log.success(`Removed ${log.bold(packageLabel(name, version2))} from cache.`);
605
+ try {
606
+ if (fs.readdirSync(pkgDir).length === 0)
607
+ fs.rmdirSync(pkgDir);
608
+ if (name.startsWith("@")) {
609
+ const scopeDir = path.join(storageDir, nameParts[0]);
610
+ if (fs.existsSync(scopeDir) && fs.readdirSync(scopeDir).length === 0) {
611
+ fs.rmdirSync(scopeDir);
612
+ }
613
+ }
614
+ } catch {
615
+ }
616
+ }
617
+ }
618
+ module2.exports = { removePackage: removePackage2 };
619
+ }
620
+ });
621
+
622
+ // package.json
623
+ var require_package = __commonJS({
624
+ "package.json"(exports2, module2) {
625
+ module2.exports = {
626
+ name: "offline-npm-manager",
627
+ version: "1.0.5",
628
+ description: "Download npm packages online, install them offline later",
629
+ bin: {
630
+ "offline-npm": "./dist/cli.cjs"
631
+ },
632
+ scripts: {
633
+ build: "esbuild bin/offline-npm.js --bundle --platform=node --format=cjs --outfile=dist/cli.cjs --external:commander",
634
+ prepublishOnly: "npm run build",
635
+ test: "node test/test.js",
636
+ dev: "node ./bin/offline-npm.js",
637
+ start: "npm run dev"
638
+ },
639
+ keywords: [
640
+ "npm",
641
+ "offline",
642
+ "package-manager",
643
+ "cli",
644
+ "offline-package-manager",
645
+ "offline-install",
646
+ "npm-cache",
647
+ "node-cli",
648
+ "dependency-cache",
649
+ "dependency-manager",
650
+ "offline-first"
651
+ ],
652
+ author: "Sagor Ahamed",
653
+ license: "MIT",
654
+ files: [
655
+ "bin",
656
+ "dist"
657
+ ],
658
+ dependencies: {
659
+ commander: "^11.1.0"
660
+ },
661
+ devDependencies: {
662
+ esbuild: "^0.19.0"
663
+ },
664
+ engines: {
665
+ node: ">=16.0.0"
666
+ },
667
+ postuninstall: `node -e "const fs=require('fs');const path=require('path');const os=require('os');const dir=path.join(os.homedir(),'.offline-npm-cache');if(fs.existsSync(dir))fs.rmSync(dir,{recursive:true,force:true});"`,
668
+ repository: {
669
+ type: "git",
670
+ url: "https://github.com/SagorAhamed251245/offline-npm-manager.git"
671
+ },
672
+ bugs: {
673
+ url: "https://github.com/SagorAhamed251245/offline-npm-manager/issues"
674
+ },
675
+ homepage: "https://github.com/SagorAhamed251245",
676
+ publishConfig: {
677
+ access: "public"
678
+ },
679
+ contributors: [
680
+ {
681
+ name: "Sagor Ahamed",
682
+ url: "https://github.com/SagorAhamed251245"
683
+ }
684
+ ],
685
+ funding: [
686
+ {
687
+ type: "github",
688
+ url: "https://github.com/sponsors/SagorAhamed251245"
689
+ }
690
+ ]
691
+ };
692
+ }
693
+ });
694
+
695
+ // bin/offline-npm.js
696
+ var { program } = require("commander");
697
+ var { addPackage } = require_add();
698
+ var { installPackage } = require_install();
699
+ var { listPackages } = require_list();
700
+ var { removePackage } = require_remove();
701
+ var { version } = require_package();
702
+ program.name("offline-npm").description(
703
+ "\u{1F4E6} Download npm packages when online, install them offline later"
704
+ ).version(version);
705
+ program.command("add <package>").description("Download a package and store it locally (requires internet)").option("-d, --deps", "Also download all dependencies recursively", false).option("-s, --storage <path>", "Custom storage directory").action(async (pkg, options) => {
706
+ await addPackage(pkg, options);
707
+ });
708
+ program.command("install <package>").description("Install a package from local offline storage").option("-s, --storage <path>", "Custom storage directory").option("--save", "Add to package.json dependencies", false).option("--save-dev", "Add to package.json devDependencies", false).action(async (pkg, options) => {
709
+ await installPackage(pkg, options);
710
+ });
711
+ program.command("list").alias("ls").description("List all locally stored packages").option("-s, --storage <path>", "Custom storage directory").action(async (options) => {
712
+ await listPackages(options);
713
+ });
714
+ program.command("remove <package>").alias("rm").description("Remove a package from local offline storage").option("-s, --storage <path>", "Custom storage directory").action(async (pkg, options) => {
715
+ await removePackage(pkg, options);
716
+ });
717
+ program.parse(process.argv);
718
+ if (!process.argv.slice(2).length) {
719
+ program.outputHelp();
720
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "offline-npm-manager",
3
+ "version": "1.0.5",
4
+ "description": "Download npm packages online, install them offline later",
5
+ "bin": {
6
+ "offline-npm": "./dist/cli.cjs"
7
+ },
8
+ "scripts": {
9
+ "build": "esbuild bin/offline-npm.js --bundle --platform=node --format=cjs --outfile=dist/cli.cjs --external:commander",
10
+ "prepublishOnly": "npm run build",
11
+ "test": "node test/test.js",
12
+ "dev": "node ./bin/offline-npm.js",
13
+ "start": "npm run dev"
14
+ },
15
+ "keywords": [
16
+ "npm",
17
+ "offline",
18
+ "package-manager",
19
+ "cli",
20
+ "offline-package-manager",
21
+ "offline-install",
22
+ "npm-cache",
23
+ "node-cli",
24
+ "dependency-cache",
25
+ "dependency-manager",
26
+ "offline-first"
27
+ ],
28
+ "author": "Sagor Ahamed",
29
+ "license": "MIT",
30
+ "files": [
31
+ "bin",
32
+ "dist"
33
+ ],
34
+ "dependencies": {
35
+ "commander": "^11.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "esbuild": "^0.19.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ },
43
+ "postuninstall": "node -e \"const fs=require('fs');const path=require('path');const os=require('os');const dir=path.join(os.homedir(),'.offline-npm-cache');if(fs.existsSync(dir))fs.rmSync(dir,{recursive:true,force:true});\"",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/SagorAhamed251245/offline-npm-manager.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/SagorAhamed251245/offline-npm-manager/issues"
50
+ },
51
+ "homepage": "https://github.com/SagorAhamed251245",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "contributors": [
56
+ {
57
+ "name": "Sagor Ahamed",
58
+ "url": "https://github.com/SagorAhamed251245"
59
+ }
60
+ ],
61
+ "funding": [
62
+ {
63
+ "type": "github",
64
+ "url": "https://github.com/sponsors/SagorAhamed251245"
65
+ }
66
+ ]
67
+ }