@adithya-naik/cmd-tracker 1.0.0

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,120 @@
1
+ /*
2
+ * stats.js
3
+ *
4
+ * Handles "tracker stats" command
5
+ *
6
+ * Shows a breakdown of saved commands:
7
+ * → Total commands saved
8
+ * → Count per category
9
+ * → Percentage per category
10
+ * → Most used category
11
+ */
12
+
13
+ const { readCommands } = require("../utils/storage");
14
+ const { isInitialized, showInitError } = require("../utils/validator");
15
+
16
+ function statsCommand() {
17
+
18
+ if (!isInitialized()) {
19
+ showInitError();
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const data = readCommands();
25
+
26
+ /*
27
+ * Calculate total commands across all categories
28
+ * Object.values() → gets all arrays from data object
29
+ * .reduce() → adds up all lengths
30
+ *
31
+ * Example:
32
+ * { git: [1,2], npm: [1] }
33
+ * → values → [[1,2], [1]]
34
+ * → reduce → 2 + 1 = 3
35
+ */
36
+ const total = Object.values(data).reduce(
37
+ (sum, commands) => sum + commands.length, 0
38
+ );
39
+
40
+ if (total === 0) {
41
+ console.log("\n📭 No commands saved yet!");
42
+ console.log("💡 Use: tracker save \"your command\"\n");
43
+ return;
44
+ }
45
+
46
+ console.log("\n📊 CMD-TRACKER — Statistics\n");
47
+ console.log("─".repeat(50));
48
+
49
+ /*
50
+ * Category icons — same as list.js for consistency
51
+ */
52
+ const icons = {
53
+ git: "🔀",
54
+ npm: "📦",
55
+ docker: "🐳",
56
+ linux: "🐧",
57
+ node: "🟢",
58
+ angular: "🔴",
59
+ python: "🐍",
60
+ others: "📌"
61
+ };
62
+
63
+ /*
64
+ * Track which category has most commands
65
+ */
66
+ let topCategory = "";
67
+ let topCount = 0;
68
+
69
+ /*
70
+ * Loop through each category and show stats
71
+ */
72
+ for (const [category, commands] of Object.entries(data)) {
73
+
74
+ /*
75
+ * Skip empty categories
76
+ */
77
+ if (commands.length === 0) continue;
78
+
79
+ /*
80
+ * Calculate percentage
81
+ * Math.round() → rounds to nearest whole number
82
+ * 2.7 → 3, 2.3 → 2
83
+ */
84
+ const percentage = Math.round((commands.length / total) * 100);
85
+
86
+ /*
87
+ * Build a simple visual bar
88
+ * "█".repeat(percentage/5) → each block = 5%
89
+ * So 50% → 10 blocks, 100% → 20 blocks
90
+ */
91
+ const bar = "█".repeat(Math.round(percentage / 5));
92
+
93
+ const icon = icons[category] || "📌";
94
+
95
+ console.log(
96
+ `\n${icon} ${category.toUpperCase().padEnd(10)} ` +
97
+ `${String(commands.length).padStart(3)} commands ` +
98
+ `${String(percentage).padStart(3)}% ${bar}`
99
+ );
100
+
101
+ /*
102
+ * Track top category
103
+ */
104
+ if (commands.length > topCount) {
105
+ topCount = commands.length;
106
+ topCategory = category;
107
+ }
108
+ }
109
+
110
+ console.log("\n" + "─".repeat(50));
111
+ console.log(`📦 Total commands : ${total}`);
112
+ console.log(`🏆 Most used : ${topCategory} (${topCount} commands)\n`);
113
+
114
+ } catch (error) {
115
+ console.log("\n❌ Error reading statistics");
116
+ console.log("💡 Try running tracker init again\n");
117
+ }
118
+ }
119
+
120
+ module.exports = { statsCommand };
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /*
2
+ * index.js — Main entry point when package is required
3
+ *
4
+ * This file is used when someone does:
5
+ * const tracker = require('@adithya-naik/cmd-tracker')
6
+ *
7
+ * We export our core utilities so developers can
8
+ * use them programmatically in their own code
9
+ */
10
+
11
+ const { saveCommand, readCommands, initStorage } = require("./utils/storage");
12
+ const { categorize } = require("./utils/categorizer");
13
+
14
+ module.exports = {
15
+ saveCommand,
16
+ readCommands,
17
+ initStorage,
18
+ categorize
19
+ };
@@ -0,0 +1,165 @@
1
+ /*
2
+ * categorizer.js
3
+ *
4
+ * This file has ONE job — look at a command and decide its category
5
+ * Example:
6
+ * "git status" → "git"
7
+ * "npm install" → "npm"
8
+ * "docker ps" → "docker"
9
+ * "ls -la" → "linux"
10
+ * "ng new my-app" → "others"
11
+ */
12
+
13
+ /*
14
+ * We define all our categories and their keywords here
15
+ * This is called a CONFIGURATION OBJECT
16
+ *
17
+ * Why keep it here separately?
18
+ * → Easy to add new categories later
19
+ * → Easy to add new keywords to existing categories
20
+ * → Everything in one place — no hunting around in code
21
+ */
22
+ const CATEGORIES = {
23
+
24
+ /*
25
+ * Any command starting with "git" goes here
26
+ * Examples: git status, git push, git commit -m "msg"
27
+ */
28
+ git: ["git"],
29
+
30
+ /*
31
+ * Any command starting with "npm" or "npx" goes here
32
+ * Examples: npm install, npm run dev, npx create-react-app
33
+ */
34
+ npm: ["npm", "npx"],
35
+
36
+ /*
37
+ * Any command starting with "docker" goes here
38
+ * Examples: docker ps, docker build, docker-compose up
39
+ */
40
+ docker: ["docker", "docker-compose"],
41
+
42
+ /*
43
+ * Common linux commands go here
44
+ * Examples: ls -la, cd projects, mkdir new-folder
45
+ */
46
+ linux: [
47
+ "ls",
48
+ "cd",
49
+ "pwd",
50
+ "mkdir",
51
+ "rm",
52
+ "cp",
53
+ "mv",
54
+ "cat",
55
+ "touch",
56
+ "chmod",
57
+ "chown",
58
+ "grep",
59
+ "find",
60
+ "echo",
61
+ "sudo",
62
+ "apt",
63
+ "curl",
64
+ "wget",
65
+ "nano",
66
+ "vim"
67
+ ],
68
+
69
+ /*
70
+ * Node.js related commands
71
+ * Examples: node index.js, nodemon server.js
72
+ */
73
+ node: ["node", "nodemon"],
74
+
75
+ /*
76
+ * Angular CLI commands
77
+ * Examples: ng new my-app, ng serve, ng generate component
78
+ */
79
+ angular: ["ng"],
80
+
81
+ /*
82
+ * Python commands
83
+ * Examples: python app.py, pip install flask
84
+ */
85
+ python: ["python", "python3", "pip", "pip3"],
86
+ };
87
+
88
+ /*
89
+ * categorize() — the main function of this file
90
+ *
91
+ * It takes a command string as input
92
+ * It returns the category name as a string
93
+ *
94
+ * @param {string} command - the terminal command typed by user
95
+ * @returns {string} - category name (git/npm/docker/linux/node/angular/python/others)
96
+ *
97
+ * Example:
98
+ * categorize("git status") → "git"
99
+ * categorize("npm install") → "npm"
100
+ * categorize("ng serve") → "angular"
101
+ * categorize("abc xyz") → "others"
102
+ */
103
+ function categorize(command) {
104
+
105
+ /*
106
+ * Safety check — if command is empty or not a string, return "others"
107
+ * This prevents crashes if something unexpected is passed in
108
+ */
109
+ if (!command || typeof command !== "string") {
110
+ return "others";
111
+ }
112
+
113
+ /*
114
+ * .trim() → removes spaces from start and end
115
+ * .toLowerCase() → converts to lowercase so "Git Status" = "git status"
116
+ * .split(" ")[0] → takes only the FIRST word
117
+ *
118
+ * Example:
119
+ * " git status " → trim → "git status"
120
+ * → toLowerCase → "git status"
121
+ * → split(" ") → ["git", "status"]
122
+ * → [0] → "git"
123
+ */
124
+ const firstWord = command.trim().toLowerCase().split(" ")[0];
125
+
126
+ /*
127
+ * Object.entries() converts our CATEGORIES object into an array like:
128
+ * [
129
+ * ["git", ["git"]],
130
+ * ["npm", ["npm", "npx"]],
131
+ * ...
132
+ * ]
133
+ *
134
+ * We loop through each [categoryName, keywords] pair
135
+ */
136
+ for (const [categoryName, keywords] of Object.entries(CATEGORIES)) {
137
+
138
+ /*
139
+ * .includes() checks if our firstWord exists in the keywords array
140
+ *
141
+ * Example:
142
+ * categoryName = "npm"
143
+ * keywords = ["npm", "npx"]
144
+ * firstWord = "npx"
145
+ * keywords.includes("npx") → true → return "npm"
146
+ */
147
+ if (keywords.includes(firstWord)) {
148
+ return categoryName;
149
+ }
150
+ }
151
+
152
+ /*
153
+ * If no category matched — return "others"
154
+ * This is our catch-all category
155
+ */
156
+ return "others";
157
+ }
158
+
159
+ /*
160
+ * module.exports → makes this function available to other files
161
+ * Without this line — no other file can use categorize()
162
+ *
163
+ * This is how Node.js shares code between files
164
+ */
165
+ module.exports = { categorize };
@@ -0,0 +1,281 @@
1
+ /*
2
+ * hook.js
3
+ *
4
+ * This file handles the shell hook setup
5
+ * Shell hook = a small script added to user's shell config
6
+ * that runs automatically after EVERY command they type
7
+ *
8
+ * Supported shells:
9
+ * → bash → hooks into ~/.bashrc
10
+ * → zsh → hooks into ~/.zshrc
11
+ * → Both are most common shells on Mac/Linux
12
+ *
13
+ * How it works:
14
+ * 1. Detect user's shell (bash or zsh)
15
+ * 2. Add a hook script to their shell config file
16
+ * 3. Hook script calls "tracker save <command>" after every command
17
+ * 4. User sources their config → automatic capture starts!
18
+ */
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+ const os = require("os");
23
+
24
+ /*
25
+ * os module — built into Node.js
26
+ * os.homedir() → gives home directory path
27
+ * Windows → C:\Users\Adithya
28
+ * Mac/Linux → /home/adithya
29
+ */
30
+ const HOME_DIR = os.homedir();
31
+
32
+ /*
33
+ * Shell config file paths
34
+ * These are standard locations for shell config files
35
+ */
36
+ const SHELL_CONFIGS = {
37
+ bash: path.join(HOME_DIR, ".bashrc"),
38
+ zsh: path.join(HOME_DIR, ".zshrc")
39
+ };
40
+
41
+ /*
42
+ * This is the actual hook script we add to shell config
43
+ * It runs after every command the user types
44
+ *
45
+ * How it works line by line:
46
+ *
47
+ * _cmd_tracker_hook() → defines a function
48
+ * LAST_CMD=$(history 1 | sed ...) → gets the last command typed
49
+ * tracker save "$LAST_CMD" → saves it using our CLI
50
+ *
51
+ * precmd() → zsh hook that runs before each prompt (zsh only)
52
+ * PROMPT_COMMAND → bash variable that runs before each prompt (bash only)
53
+ */
54
+ /*
55
+ * HOOK_SCRIPT — the shell script we add to user's config
56
+ *
57
+ * IMPORTANT: We use regular string (single quotes) NOT template literal
58
+ * Because ${PROMPT_COMMAND} is a BASH variable not a JavaScript variable
59
+ * If we use backticks → JavaScript tries to read PROMPT_COMMAND as JS variable
60
+ * If we use regular string → it stays as plain text ✅
61
+ */
62
+ /*
63
+ * HOOK_SCRIPT — shell script added to user's config
64
+ *
65
+ * The sed command had escaped quote issues before
66
+ * Fix: use [[:space:]] instead of [ ] and avoid nested quotes
67
+ * This is cleaner and works on all bash/zsh versions
68
+ */
69
+ const HOOK_SCRIPT = [
70
+ "",
71
+ "# cmd-tracker shell hook — auto saves every command you type",
72
+ "_cmd_tracker_hook() {",
73
+ " local LAST_CMD",
74
+ " LAST_CMD=$(history 1 | sed 's/^[[:space:]]*[0-9]*[[:space:]]*//')",
75
+ ' if [ -n "$LAST_CMD" ]; then',
76
+ ' tracker save "$LAST_CMD" > /dev/null 2>&1',
77
+ " fi",
78
+ "}",
79
+ "",
80
+ "# For zsh",
81
+ 'if [ -n "$ZSH_VERSION" ]; then',
82
+ " autoload -U add-zsh-hook",
83
+ " add-zsh-hook precmd _cmd_tracker_hook",
84
+ "fi",
85
+ "",
86
+ "# For bash",
87
+ 'if [ -n "$BASH_VERSION" ]; then',
88
+ ' PROMPT_COMMAND="_cmd_tracker_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"',
89
+ "fi",
90
+ ""
91
+ ].join("\n");
92
+
93
+ /*
94
+ * HOOK_MARKER — unique string we add to identify our hook
95
+ * We use this to:
96
+ * → Check if hook already exists (avoid duplicates)
97
+ * → Find and remove hook when user runs tracker unhook
98
+ */
99
+ const HOOK_MARKER = "# cmd-tracker shell hook — auto saves every command you type";
100
+
101
+ /*
102
+ * detectShell() — detects which shell user is running
103
+ *
104
+ * process.env.SHELL → environment variable containing shell path
105
+ * Example:
106
+ * "/bin/bash" → bash
107
+ * "/bin/zsh" → zsh
108
+ *
109
+ * @returns {string} — "bash", "zsh", or "unknown"
110
+ */
111
+ function detectShell() {
112
+ const shell = process.env.SHELL || "";
113
+
114
+ if (shell.includes("zsh")) return "zsh";
115
+ if (shell.includes("bash")) return "bash";
116
+
117
+ /*
118
+ * On Windows — check SHELL or ComSpec
119
+ * Most Windows users use bash via Git Bash
120
+ */
121
+ const comspec = process.env.ComSpec || "";
122
+ if (comspec.includes("cmd.exe")) return "unknown";
123
+
124
+ return "bash"; // default to bash
125
+ }
126
+
127
+ /*
128
+ * getConfigFile() — returns path to shell config file
129
+ *
130
+ * @param {string} shell — "bash" or "zsh"
131
+ * @returns {string} — full path to config file
132
+ */
133
+ function getConfigFile(shell) {
134
+ return SHELL_CONFIGS[shell] || SHELL_CONFIGS.bash;
135
+ }
136
+
137
+ /*
138
+ * installHook() — adds hook script to shell config
139
+ *
140
+ * @returns {object} — { success: true/false, shell, configFile, message }
141
+ */
142
+ function installHook() {
143
+
144
+ try {
145
+ const shell = detectShell();
146
+
147
+ /*
148
+ * If shell is unknown — we can't install hook
149
+ * This happens on Windows CMD/PowerShell
150
+ */
151
+ if (shell === "unknown") {
152
+ return {
153
+ success: false,
154
+ message: "❌ Automatic hook not supported on Windows CMD/PowerShell\n💡 Use Git Bash or WSL for automatic capture"
155
+ };
156
+ }
157
+
158
+ const configFile = getConfigFile(shell);
159
+
160
+ /*
161
+ * Check if hook already installed
162
+ * Look for our unique marker in the config file
163
+ */
164
+ if (fs.existsSync(configFile)) {
165
+ const content = fs.readFileSync(configFile, "utf-8");
166
+
167
+ if (content.includes(HOOK_MARKER)) {
168
+ return {
169
+ success: false,
170
+ message: `⚠️ Hook already installed in ${configFile}`
171
+ };
172
+ }
173
+ }
174
+
175
+ /*
176
+ * Append hook script to shell config file
177
+ * appendFileSync → adds to end without deleting existing content
178
+ */
179
+ fs.appendFileSync(configFile, HOOK_SCRIPT);
180
+
181
+ return {
182
+ success: true,
183
+ shell,
184
+ configFile,
185
+ message: `✅ Hook installed in ${configFile}`
186
+ };
187
+
188
+ } catch (error) {
189
+
190
+ if (error.code === "EACCES") {
191
+ return {
192
+ success: false,
193
+ message: "❌ Permission denied! Try with sudo/admin permissions"
194
+ };
195
+ }
196
+
197
+ return {
198
+ success: false,
199
+ message: `❌ Failed to install hook: ${error.message}`
200
+ };
201
+ }
202
+ }
203
+
204
+ /*
205
+ * removeHook() — removes hook script from shell config
206
+ *
207
+ * @returns {object} — { success: true/false, message }
208
+ */
209
+ function removeHook() {
210
+
211
+ try {
212
+ const shell = detectShell();
213
+ const configFile = getConfigFile(shell);
214
+
215
+ if (!fs.existsSync(configFile)) {
216
+ return {
217
+ success: false,
218
+ message: "❌ Shell config file not found"
219
+ };
220
+ }
221
+
222
+ const content = fs.readFileSync(configFile, "utf-8");
223
+
224
+ /*
225
+ * Check if hook exists
226
+ */
227
+ if (!content.includes(HOOK_MARKER)) {
228
+ return {
229
+ success: false,
230
+ message: "⚠️ Hook not found in shell config"
231
+ };
232
+ }
233
+
234
+ /*
235
+ * Remove hook script from config
236
+ * Split by marker → take everything before it
237
+ * Then trim trailing whitespace
238
+ */
239
+ const newContent = content.split(HOOK_MARKER)[0].trimEnd() + "\n";
240
+ fs.writeFileSync(configFile, newContent);
241
+
242
+ return {
243
+ success: true,
244
+ configFile,
245
+ message: `✅ Hook removed from ${configFile}`
246
+ };
247
+
248
+ } catch (error) {
249
+ return {
250
+ success: false,
251
+ message: `❌ Failed to remove hook: ${error.message}`
252
+ };
253
+ }
254
+ }
255
+
256
+ /*
257
+ * isHookInstalled() — checks if hook is already installed
258
+ *
259
+ * @returns {boolean}
260
+ */
261
+ function isHookInstalled() {
262
+ try {
263
+ const shell = detectShell();
264
+ const configFile = getConfigFile(shell);
265
+
266
+ if (!fs.existsSync(configFile)) return false;
267
+
268
+ const content = fs.readFileSync(configFile, "utf-8");
269
+ return content.includes(HOOK_MARKER);
270
+
271
+ } catch (error) {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ module.exports = {
277
+ installHook,
278
+ removeHook,
279
+ isHookInstalled,
280
+ detectShell
281
+ };