@voxframeworks/linetools 1.0.1 → 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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "liveServer.settings.port": 5501
3
+ }
package/main.js CHANGED
@@ -4,10 +4,13 @@ const touch = require('./tools/touch.js');
4
4
  const mv = require('./tools/mv.js');
5
5
  const tar = require('./tools/tar.js');
6
6
  const rmdir = require('./tools/rmdir.js');
7
+ const chmod = require('./tools/chmod.js');
7
8
  module.exports = {
8
9
  mkdir : mkdir,
9
10
  cp : cp,
10
11
  touch : touch,
11
12
  mv : mv,
12
- rmdir : rmdir
13
+ rmdir : rmdir,
14
+ tar : tar,
15
+ chmod : chmod
13
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxframeworks/linetools",
3
- "version": "1.0.1",
3
+ "version": "1.0.5",
4
4
  "description": "command line tools for js",
5
5
  "main": "main.js",
6
6
  "scripts": {
package/readme.md CHANGED
@@ -20,8 +20,20 @@ The list of commands we have is:
20
20
  • cp
21
21
  • mv
22
22
  • touch
23
- • tar (not working)
24
-
23
+ • tar (fixed)
24
+ -----------------
25
+ Tar usage:
26
+ linetools.tar('testOUT', 'testDir', 'rezip')
27
+ TestDir = Directory to be zipped
28
+ TestOut = Directory for the zip to be put in
29
+ rezip = zip
30
+ unzip = unzip
31
+ ------------------
32
+ Chmod tips:
33
+ 7 = read, write, execute
34
+ 5 = read, execute
35
+ 0 = none
36
+ ------------------
25
37
  We will be adding more soon!
26
38
 
27
39
  By VoxFrameworks
package/test.js CHANGED
@@ -1,2 +1,3 @@
1
1
  const linetools = require('linetools');
2
2
  linetools.tar('testOUT', 'testDir', 'rezip')
3
+ linetools.chmod('testDir', '755')
package/tools/chmod.js ADDED
@@ -0,0 +1,122 @@
1
+ // lib/chmod.js
2
+ /**
3
+ * Change the permissions of a file or directory.
4
+ *
5
+ * @param {string} targetPath – Path to file or directory.
6
+ * @param {string|number} mode – Permission bits.
7
+ * * If a string: interpreted as octal (e.g. '755')
8
+ * * If a number: used directly (e.g. 0o755)
9
+ * @param {object} [options] – Optional flags.
10
+ * @param {boolean} [options.recursive] – If true and target is a directory,
11
+ * change permissions of every descendant.
12
+ *
13
+ * @returns {Promise<void>}
14
+ *
15
+ * @example
16
+ * // change a single file
17
+ * await chmod('tmp/foo.txt', '644');
18
+ *
19
+ * // change a folder recursively
20
+ * await chmod('tmp/myDir', 0o755, { recursive: true });
21
+ */
22
+ const { promises: fs } = require('fs');
23
+ const path = require('path');
24
+
25
+ /**
26
+ * Convert a user‑provided mode to a proper numeric value.
27
+ * Accepts:
28
+ * • string like "755", "0644", "0o777"
29
+ * • number (already a mode)
30
+ */
31
+ function normalizeMode(mode) {
32
+ if (typeof mode === 'number') return mode; // already numeric
33
+ if (typeof mode !== 'string')
34
+ throw new TypeError('Mode must be a string or a number');
35
+
36
+ // Strip common prefixes (0o, 0, etc.) and parse as octal.
37
+ const clean = mode.replace(/^0o?/, '');
38
+ const parsed = parseInt(clean, 8);
39
+ if (Number.isNaN(parsed))
40
+ throw new Error(`Invalid permission mode: "${mode}"`);
41
+ return parsed;
42
+ }
43
+
44
+ /**
45
+ * Recursively walk a directory tree and collect *all* entries (files + dirs).
46
+ * Returns an array of absolute paths.
47
+ */
48
+ async function walkRecursively(dir) {
49
+ const result = [];
50
+
51
+ async function walk(current) {
52
+ const entries = await fs.readdir(current, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const full = path.join(current, entry.name);
55
+ result.push(full);
56
+ if (entry.isDirectory()) await walk(full);
57
+ }
58
+ }
59
+
60
+ await walk(dir);
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * The public API – async function.
66
+ */
67
+ async function chmod(targetPath, mode, options = {}) {
68
+ const normalizedMode = normalizeMode(mode);
69
+ const { recursive = false } = options;
70
+
71
+ // Verify the target exists first – otherwise `fs.chmod` would throw a less‑friendly ENOENT.
72
+ let stats;
73
+ try {
74
+ stats = await fs.lstat(targetPath);
75
+ } catch (err) {
76
+ throw new Error(`Path does not exist: ${targetPath}`);
77
+ }
78
+
79
+ // --------------------------------------------------------------
80
+ // 1️⃣ Non‑recursive case (file OR directory)
81
+ // --------------------------------------------------------------
82
+ if (!recursive) {
83
+ await fs.chmod(targetPath, normalizedMode);
84
+ return;
85
+ }
86
+
87
+ // --------------------------------------------------------------
88
+ // 2️⃣ Recursive case – target must be a directory
89
+ // --------------------------------------------------------------
90
+ if (!stats.isDirectory())
91
+ throw new Error(
92
+ `Recursive chmod can only be used on directories. "${targetPath}" is not a directory.`
93
+ );
94
+
95
+ // Change the *directory* itself first (so it matches the children afterwards)
96
+ await fs.chmod(targetPath, normalizedMode);
97
+
98
+ // Walk the tree and chmod everything we find.
99
+ const allPaths = await walkRecursively(targetPath);
100
+
101
+ // Use `Promise.all` but limit concurrency a bit to avoid overwhelming the FS on huge trees.
102
+ const MAX_CONCURRENT = 100;
103
+ const queue = [...allPaths];
104
+ const workers = Array.from({ length: MAX_CONCURRENT }, async () => {
105
+ while (queue.length) {
106
+ const p = queue.pop();
107
+ try {
108
+ await fs.chmod(p, normalizedMode);
109
+ } catch (e) {
110
+ // We *continue* on errors so the rest of the tree still gets processed.
111
+ console.warn(`⚠️ Could not chmod "${p}": ${e.message}`);
112
+ }
113
+ }
114
+ });
115
+
116
+ await Promise.all(workers);
117
+ }
118
+
119
+ /* ------------------------------------------------------------------ */
120
+ /* Export for the public API */
121
+ /* ------------------------------------------------------------------ */
122
+ module.exports = chmod;
package/tools/tar.js CHANGED
@@ -1,69 +1,141 @@
1
- const tarModule = require('tar');
1
+ // lib/tar.js
2
+ /**
3
+ * Tiny wrapper around the `tar` package.
4
+ *
5
+ * linetools.tar(target, source, type)
6
+ *
7
+ * target – where the archive should be written (for "rezip")
8
+ * OR the archive to read (for "unzip").
9
+ * It may be:
10
+ * • a full filename, e.g. "out/pkg.tar.gz"
11
+ * • a directory name – the function will create
12
+ * "archive.tar" (or "archive.tar.gz") inside it.
13
+ *
14
+ * source – folder to pack / file to pack (rezip) OR
15
+ * destination folder for extraction (unzip)
16
+ *
17
+ * type – "rezip" (create) or "unzip" (extract)
18
+ *
19
+ * Supports g‑zipped archives when the target ends with ".gz" or ".tar.gz".
20
+ *
21
+ * @param {string} targetPath
22
+ * @param {string} sourcePath
23
+ * @param {'unzip'|'rezip'} type
24
+ * @returns {Promise<void>}
25
+ */
26
+ const tar = require('tar');
2
27
  const fs = require('fs').promises;
3
28
  const path = require('path');
4
29
 
30
+ async function ensureDir(dir) {
31
+ await fs.mkdir(dir, { recursive: true });
32
+ }
33
+
34
+ /**
35
+ * Resolve a *file* name for the archive.
36
+ * If `targetPath` already points to an existing directory,
37
+ * a default filename (`archive.tar` or `archive.tar.gz`) is added.
38
+ * If no extension is supplied, `.tar` (or `.tar.gz` for gzip) is appended.
39
+ */
40
+ async function resolveTargetFile(targetPath, mode) {
41
+ const stat = await fs.lstat(targetPath).catch(() => null);
42
+
43
+ // ── target is an existing directory → add a default filename ──
44
+ if (stat && stat.isDirectory()) {
45
+ if (mode !== 'rezip')
46
+ throw new Error(
47
+ 'When extracting, targetPath must be a file (the tarball).'
48
+ );
49
+ const defaultName = targetPath.endsWith('.gz')
50
+ ? 'archive.tar.gz'
51
+ : 'archive.tar';
52
+ return path.join(targetPath, defaultName);
53
+ }
54
+
55
+ // ── target does NOT exist – make sure its parent directory exists ──
56
+ const parent = path.dirname(targetPath);
57
+ await ensureDir(parent);
58
+
59
+ // ── does it already have a proper .tar/.tar.gz extension? ──
60
+ const hasTarExt = targetPath.endsWith('.tar') || targetPath.endsWith('.tar.gz');
61
+ if (hasTarExt) return targetPath;
62
+
63
+ // ── no extension – infer one. If the caller added a trailing ".gz"
64
+ // (e.g. "out/.gz") we treat it as a request for gzip compression.
65
+ const inferred = targetPath + (mode === 'rezip' && targetPath.endsWith('.gz')
66
+ ? '.tar.gz'
67
+ : '.tar');
68
+ return inferred;
69
+ }
70
+
5
71
  /**
6
- * Handles tar archive operations (unzip/rezip) for files and folders.
7
- * @param {string} targetPath - The path to the tarball file (output for rezip, input for unzip).
8
- * @param {string} sourcePath - The source directory/files for rezip, or the destination directory for unzip.
9
- * @param {'unzip' | 'rezip'} type - The operation type.
72
+ * Core operation create (`rezip`) or extract (`unzip`) a tarball.
10
73
  */
11
- async function tar(targetPath, sourcePath, type) {
12
- try {
13
- if (type === 'unzip') {
14
- // Ensure the destination directory exists
15
- await fs.mkdir(sourcePath, { recursive: true });
16
- console.log(`Starting extraction of '${targetPath}' to '${sourcePath}'...`);
17
- // Extracting from a tarball (handles .tar, .tar.gz automatically with modern 'tar')
18
- await tarModule.extract({
19
- file: targetPath,
20
- cwd: sourcePath, // Extract *into* this directory
21
- });
22
- console.log('Extraction completed successfully!');
23
- } else if (type === 'rezip') {
24
- const tarballDir = path.dirname(targetPath);
25
- await fs.mkdir(tarballDir, { recursive: true }); // Ensure output dir exists
26
-
27
- // For zipping folders, use tar.pack() with streams for better control & recursion
28
- // or tar.create() if you prefer the object API and ensure 'gzip: true' for .tar.gz
29
- console.log(`Starting creation of tarball '${targetPath}' from '${sourcePath}'...`);
30
- // Use tar.create or tar.c for creating streams
31
- const archiveStream = tarModule.create({ gzip: targetPath.endsWith('.gz') }, sourcePath);
32
-
33
- fileHandle = await fs.open(targetPath, 'w');
34
-
35
- // Create the write stream from the file handle
36
- const writeStream = fileHandle.createWriteStream();
37
-
38
- // If you need .tar.gz, pipe through gzip stream
39
- if (targetPath.endsWith('.gz')) {
40
- const zlib = require('zlib');
41
- await new Promise((resolve, reject) => {
42
- archiveStream
43
- .pipe(zlib.createGzip()) // Add gzip compression
44
- .pipe(outputStream)
45
- .on('finish', resolve)
46
- .on('error', reject);
47
- });
48
- } else {
49
- // For plain .tar
50
- await new Promise((resolve, reject) => {
51
- archiveStream
52
- .pipe(outputStream)
53
- .on('finish', resolve)
54
- .on('error', reject);
55
- });
56
- }
57
-
58
- console.log('Tarball creation completed successfully!');
59
-
60
- } else {
61
- throw new Error('Invalid type specified. Use "unzip" or "rezip".');
62
- }
63
- } catch (error) {
64
- console.error(`Failed to perform tar operation (${type}):`, error.message);
65
- throw error; // Rethrow to allow the calling function to handle the error
66
- }
74
+ async function tarOperation(targetPath, sourcePath, type) {
75
+ // -----------------------------------------------------------------
76
+ // 1️⃣ UNZIP – extract archive into a directory
77
+ // -----------------------------------------------------------------
78
+ if (type === 'unzip') {
79
+ await ensureDir(sourcePath);
80
+ console.log(`🔽 Extracting '${targetPath}' '${sourcePath}' …`);
81
+
82
+ await tar.extract({
83
+ file: targetPath,
84
+ cwd: sourcePath, // extract *into* this folder
85
+ });
86
+
87
+ console.log('✅ Extraction finished.');
88
+ return;
89
+ }
90
+
91
+ // -----------------------------------------------------------------
92
+ // 2️⃣ REZIP create a tarball from a folder *or* a single file
93
+ // -----------------------------------------------------------------
94
+ if (type === 'rezip') {
95
+ // Resolve a real filename for the archive (adds .tar/.tar.gz if needed)
96
+ const finalTarget = await resolveTargetFile(targetPath, 'rezip');
97
+
98
+ // Determine if the source is a file or a directory
99
+ const srcStat = await fs.lstat(sourcePath).catch(() => {
100
+ throw new Error(`Source path does not exist: ${sourcePath}`);
101
+ });
102
+
103
+ // tar.create() needs:
104
+ // - cwd : where the files are looked up
105
+ // - file: the output tarball
106
+ // - gzip: true if we want *.tar.gz
107
+ const tarOpts = {
108
+ cwd: srcStat.isDirectory() ? sourcePath : path.dirname(sourcePath),
109
+ file: finalTarget,
110
+ gzip: finalTarget.endsWith('.gz'), // true for .tar.gz, false for .tar
111
+ portable: true, // reproducible timestamps
112
+ };
113
+
114
+ // Decide which entries to feed to tar:
115
+ // • directory → pack everything inside it (['.'])
116
+ // • file → pack only that one file (basename)
117
+ const entries = srcStat.isDirectory()
118
+ ? ['.']
119
+ : [path.basename(sourcePath)];
120
+
121
+ console.log(
122
+ `📦 Creating ${tarOpts.gzip ? 'gzip‑compressed ' : ''}` +
123
+ `tarball '${finalTarget}' from ${srcStat.isDirectory() ? 'folder' : 'file'} ` +
124
+ `'${sourcePath}' …`
125
+ );
126
+
127
+ // `tar.create` returns a promise that resolves when the stream finishes.
128
+ await tar.create(tarOpts, entries);
129
+
130
+ console.log('✅ Tarball created successfully.');
131
+ return;
132
+ }
133
+
134
+ // -----------------------------------------------------------------
135
+ // 3️⃣ WRONG TYPE
136
+ // -----------------------------------------------------------------
137
+ throw new Error('Invalid type – use "unzip" or "rezip".');
67
138
  }
68
139
 
69
- module.exports = tar;
140
+ // Export the function that the consumer will receive as `linetools.tar`
141
+ module.exports = tarOperation;