notoken-core 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.
Files changed (118) hide show
  1. package/config/file-hints.json +255 -0
  2. package/config/hosts.json +14 -0
  3. package/config/intents.json +3920 -0
  4. package/config/playbooks.json +112 -0
  5. package/config/rules.json +100 -0
  6. package/dist/agents/agentSpawner.d.ts +56 -0
  7. package/dist/agents/agentSpawner.js +180 -0
  8. package/dist/agents/planner.d.ts +40 -0
  9. package/dist/agents/planner.js +175 -0
  10. package/dist/agents/playbookRunner.d.ts +45 -0
  11. package/dist/agents/playbookRunner.js +120 -0
  12. package/dist/agents/taskRunner.d.ts +61 -0
  13. package/dist/agents/taskRunner.js +142 -0
  14. package/dist/context/history.d.ts +36 -0
  15. package/dist/context/history.js +115 -0
  16. package/dist/conversation/coreference.d.ts +27 -0
  17. package/dist/conversation/coreference.js +147 -0
  18. package/dist/conversation/secrets.d.ts +43 -0
  19. package/dist/conversation/secrets.js +129 -0
  20. package/dist/conversation/store.d.ts +94 -0
  21. package/dist/conversation/store.js +184 -0
  22. package/dist/execution/git.d.ts +11 -0
  23. package/dist/execution/git.js +146 -0
  24. package/dist/execution/ssh.d.ts +2 -0
  25. package/dist/execution/ssh.js +17 -0
  26. package/dist/handlers/executor.d.ts +8 -0
  27. package/dist/handlers/executor.js +216 -0
  28. package/dist/healing/claudeHealer.d.ts +17 -0
  29. package/dist/healing/claudeHealer.js +300 -0
  30. package/dist/healing/patchPromoter.d.ts +25 -0
  31. package/dist/healing/patchPromoter.js +118 -0
  32. package/dist/healing/ruleBuilder.d.ts +5 -0
  33. package/dist/healing/ruleBuilder.js +111 -0
  34. package/dist/healing/ruleRepairer.d.ts +8 -0
  35. package/dist/healing/ruleRepairer.js +29 -0
  36. package/dist/healing/ruleValidator.d.ts +22 -0
  37. package/dist/healing/ruleValidator.js +145 -0
  38. package/dist/healing/runHealer.d.ts +11 -0
  39. package/dist/healing/runHealer.js +74 -0
  40. package/dist/index.d.ts +51 -0
  41. package/dist/index.js +62 -0
  42. package/dist/intents/catalog.d.ts +4 -0
  43. package/dist/intents/catalog.js +7 -0
  44. package/dist/nlp/disambiguate.d.ts +2 -0
  45. package/dist/nlp/disambiguate.js +46 -0
  46. package/dist/nlp/fuzzyResolver.d.ts +14 -0
  47. package/dist/nlp/fuzzyResolver.js +108 -0
  48. package/dist/nlp/llmFallback.d.ts +63 -0
  49. package/dist/nlp/llmFallback.js +338 -0
  50. package/dist/nlp/llmParser.d.ts +8 -0
  51. package/dist/nlp/llmParser.js +118 -0
  52. package/dist/nlp/multiClassifier.d.ts +39 -0
  53. package/dist/nlp/multiClassifier.js +181 -0
  54. package/dist/nlp/parseIntent.d.ts +2 -0
  55. package/dist/nlp/parseIntent.js +34 -0
  56. package/dist/nlp/ruleParser.d.ts +2 -0
  57. package/dist/nlp/ruleParser.js +234 -0
  58. package/dist/nlp/semantic.d.ts +104 -0
  59. package/dist/nlp/semantic.js +419 -0
  60. package/dist/nlp/uncertainty.d.ts +42 -0
  61. package/dist/nlp/uncertainty.js +103 -0
  62. package/dist/parsers/apacheParser.d.ts +50 -0
  63. package/dist/parsers/apacheParser.js +152 -0
  64. package/dist/parsers/bindParser.d.ts +40 -0
  65. package/dist/parsers/bindParser.js +189 -0
  66. package/dist/parsers/envFile.d.ts +39 -0
  67. package/dist/parsers/envFile.js +128 -0
  68. package/dist/parsers/fileFinder.d.ts +30 -0
  69. package/dist/parsers/fileFinder.js +226 -0
  70. package/dist/parsers/index.d.ts +27 -0
  71. package/dist/parsers/index.js +193 -0
  72. package/dist/parsers/jsonParser.d.ts +16 -0
  73. package/dist/parsers/jsonParser.js +57 -0
  74. package/dist/parsers/nginxParser.d.ts +47 -0
  75. package/dist/parsers/nginxParser.js +161 -0
  76. package/dist/parsers/passwd.d.ts +25 -0
  77. package/dist/parsers/passwd.js +41 -0
  78. package/dist/parsers/shadow.d.ts +23 -0
  79. package/dist/parsers/shadow.js +50 -0
  80. package/dist/parsers/yamlParser.d.ts +13 -0
  81. package/dist/parsers/yamlParser.js +54 -0
  82. package/dist/policy/confirm.d.ts +2 -0
  83. package/dist/policy/confirm.js +29 -0
  84. package/dist/policy/safety.d.ts +4 -0
  85. package/dist/policy/safety.js +32 -0
  86. package/dist/types/intent.d.ts +205 -0
  87. package/dist/types/intent.js +32 -0
  88. package/dist/types/rules.d.ts +237 -0
  89. package/dist/types/rules.js +50 -0
  90. package/dist/utils/analysis.d.ts +25 -0
  91. package/dist/utils/analysis.js +307 -0
  92. package/dist/utils/autoBackup.d.ts +43 -0
  93. package/dist/utils/autoBackup.js +144 -0
  94. package/dist/utils/config.d.ts +11 -0
  95. package/dist/utils/config.js +32 -0
  96. package/dist/utils/dirAnalysis.d.ts +23 -0
  97. package/dist/utils/dirAnalysis.js +192 -0
  98. package/dist/utils/explain.d.ts +8 -0
  99. package/dist/utils/explain.js +145 -0
  100. package/dist/utils/logger.d.ts +5 -0
  101. package/dist/utils/logger.js +29 -0
  102. package/dist/utils/output.d.ts +2 -0
  103. package/dist/utils/output.js +26 -0
  104. package/dist/utils/paths.d.ts +26 -0
  105. package/dist/utils/paths.js +47 -0
  106. package/dist/utils/permissions.d.ts +64 -0
  107. package/dist/utils/permissions.js +298 -0
  108. package/dist/utils/platform.d.ts +53 -0
  109. package/dist/utils/platform.js +253 -0
  110. package/dist/utils/smartFile.d.ts +29 -0
  111. package/dist/utils/smartFile.js +188 -0
  112. package/dist/utils/spinner.d.ts +53 -0
  113. package/dist/utils/spinner.js +140 -0
  114. package/dist/utils/verbose.d.ts +27 -0
  115. package/dist/utils/verbose.js +131 -0
  116. package/dist/utils/wslPaths.d.ts +31 -0
  117. package/dist/utils/wslPaths.js +145 -0
  118. package/package.json +39 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * OS/distro/platform detection.
3
+ *
4
+ * Detects:
5
+ * - Linux distro (Ubuntu, Debian, CentOS, RHEL, Fedora, Arch, Alpine, Amazon Linux)
6
+ * - macOS
7
+ * - Windows (PowerShell, cmd, WSL)
8
+ * - Package manager (apt, dnf, yum, pacman, apk, brew, choco)
9
+ * - Init system (systemd, sysvinit, openrc)
10
+ * - Shell (bash, zsh, fish, powershell, cmd)
11
+ *
12
+ * Works locally and remotely via SSH.
13
+ */
14
+ import { execSync } from "node:child_process";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { platform as osPlatform, release as osRelease } from "node:os";
17
+ let cachedLocal = null;
18
+ /**
19
+ * Detect the local platform.
20
+ */
21
+ export function detectLocalPlatform() {
22
+ if (cachedLocal)
23
+ return cachedLocal;
24
+ const os = osPlatform();
25
+ if (os === "win32") {
26
+ cachedLocal = detectWindows();
27
+ }
28
+ else if (os === "darwin") {
29
+ cachedLocal = detectMacOS();
30
+ }
31
+ else {
32
+ cachedLocal = detectLinux();
33
+ }
34
+ return cachedLocal;
35
+ }
36
+ function detectLinux() {
37
+ const kernel = osRelease();
38
+ const isWSL = kernel.toLowerCase().includes("microsoft") || kernel.toLowerCase().includes("wsl");
39
+ const arch = tryExec("uname -m") ?? "unknown";
40
+ const shell = process.env.SHELL ?? "unknown";
41
+ // Read /etc/os-release for distro info
42
+ let distro = "unknown";
43
+ let distroVersion = "";
44
+ let distroFamily = "unknown";
45
+ if (existsSync("/etc/os-release")) {
46
+ const content = readFileSync("/etc/os-release", "utf-8");
47
+ distro = extractField(content, "PRETTY_NAME") ?? extractField(content, "NAME") ?? "unknown";
48
+ distroVersion = extractField(content, "VERSION_ID") ?? "";
49
+ const id = (extractField(content, "ID") ?? "").toLowerCase();
50
+ const idLike = (extractField(content, "ID_LIKE") ?? "").toLowerCase();
51
+ if (id === "ubuntu" || id === "debian" || idLike.includes("debian")) {
52
+ distroFamily = "debian";
53
+ }
54
+ else if (id === "centos" || id === "rhel" || id === "fedora" || id === "rocky" || id === "alma" || id === "amzn" || idLike.includes("rhel") || idLike.includes("fedora")) {
55
+ distroFamily = "rhel";
56
+ }
57
+ else if (id === "arch" || idLike.includes("arch")) {
58
+ distroFamily = "arch";
59
+ }
60
+ else if (id === "alpine") {
61
+ distroFamily = "alpine";
62
+ }
63
+ }
64
+ // Detect package manager
65
+ let packageManager = "unknown";
66
+ if (commandExists("apt-get"))
67
+ packageManager = "apt";
68
+ else if (commandExists("dnf"))
69
+ packageManager = "dnf";
70
+ else if (commandExists("yum"))
71
+ packageManager = "yum";
72
+ else if (commandExists("pacman"))
73
+ packageManager = "pacman";
74
+ else if (commandExists("apk"))
75
+ packageManager = "apk";
76
+ // Detect init system
77
+ let initSystem = "unknown";
78
+ if (existsSync("/run/systemd/system") || commandExists("systemctl"))
79
+ initSystem = "systemd";
80
+ else if (existsSync("/etc/init.d"))
81
+ initSystem = "sysvinit";
82
+ else if (commandExists("rc-service"))
83
+ initSystem = "openrc";
84
+ return {
85
+ os: "linux",
86
+ distro,
87
+ distroVersion,
88
+ distroFamily,
89
+ kernel,
90
+ isWSL,
91
+ shell,
92
+ packageManager,
93
+ initSystem,
94
+ arch,
95
+ };
96
+ }
97
+ function detectMacOS() {
98
+ const version = tryExec("sw_vers -productVersion") ?? "";
99
+ return {
100
+ os: "darwin",
101
+ distro: `macOS ${version}`,
102
+ distroVersion: version,
103
+ distroFamily: "macos",
104
+ kernel: osRelease(),
105
+ isWSL: false,
106
+ shell: process.env.SHELL ?? "zsh",
107
+ packageManager: commandExists("brew") ? "brew" : "unknown",
108
+ initSystem: "launchd",
109
+ arch: tryExec("uname -m") ?? "unknown",
110
+ };
111
+ }
112
+ function detectWindows() {
113
+ const isPS = !!process.env.PSModulePath;
114
+ return {
115
+ os: "windows",
116
+ distro: `Windows ${osRelease()}`,
117
+ distroVersion: osRelease(),
118
+ distroFamily: "windows",
119
+ kernel: osRelease(),
120
+ isWSL: false,
121
+ shell: isPS ? "powershell" : "cmd",
122
+ packageManager: commandExists("choco") ? "choco" : "unknown",
123
+ initSystem: "unknown",
124
+ arch: process.arch,
125
+ };
126
+ }
127
+ /**
128
+ * Detect platform on a remote host via SSH.
129
+ */
130
+ export async function detectRemotePlatform(environment) {
131
+ const { runRemoteCommand } = await import("../execution/ssh.js");
132
+ try {
133
+ const osRelease = await runRemoteCommand(environment, "cat /etc/os-release 2>/dev/null || echo 'UNKNOWN'");
134
+ const kernel = (await runRemoteCommand(environment, "uname -r")).trim();
135
+ const arch = (await runRemoteCommand(environment, "uname -m")).trim();
136
+ const shell = (await runRemoteCommand(environment, "echo $SHELL")).trim();
137
+ let distro = "unknown";
138
+ let distroVersion = "";
139
+ let distroFamily = "unknown";
140
+ if (!osRelease.includes("UNKNOWN")) {
141
+ distro = extractField(osRelease, "PRETTY_NAME") ?? "unknown";
142
+ distroVersion = extractField(osRelease, "VERSION_ID") ?? "";
143
+ const id = (extractField(osRelease, "ID") ?? "").toLowerCase();
144
+ const idLike = (extractField(osRelease, "ID_LIKE") ?? "").toLowerCase();
145
+ if (id === "ubuntu" || id === "debian" || idLike.includes("debian"))
146
+ distroFamily = "debian";
147
+ else if (["centos", "rhel", "fedora", "rocky", "alma", "amzn"].includes(id) || idLike.includes("rhel"))
148
+ distroFamily = "rhel";
149
+ else if (id === "arch" || idLike.includes("arch"))
150
+ distroFamily = "arch";
151
+ else if (id === "alpine")
152
+ distroFamily = "alpine";
153
+ }
154
+ // Detect package manager remotely
155
+ const pmCheck = await runRemoteCommand(environment, "command -v apt-get >/dev/null && echo apt || command -v dnf >/dev/null && echo dnf || command -v yum >/dev/null && echo yum || command -v pacman >/dev/null && echo pacman || command -v apk >/dev/null && echo apk || echo unknown");
156
+ const packageManager = pmCheck.trim();
157
+ const initCheck = await runRemoteCommand(environment, "test -d /run/systemd/system && echo systemd || test -d /etc/init.d && echo sysvinit || echo unknown");
158
+ const initSystem = initCheck.trim();
159
+ const isWSL = kernel.toLowerCase().includes("microsoft");
160
+ return { os: "linux", distro, distroVersion, distroFamily, kernel, isWSL, shell, packageManager, initSystem, arch };
161
+ }
162
+ catch {
163
+ return { os: "unknown", distro: "unknown", distroVersion: "", distroFamily: "unknown", kernel: "", isWSL: false, shell: "", packageManager: "unknown", initSystem: "unknown", arch: "" };
164
+ }
165
+ }
166
+ /**
167
+ * Get the correct install command for the detected platform.
168
+ */
169
+ export function getInstallCommand(pkg, platform) {
170
+ switch (platform.packageManager) {
171
+ case "apt": return `sudo apt-get update && sudo apt-get install -y ${pkg}`;
172
+ case "dnf": return `sudo dnf install -y ${pkg}`;
173
+ case "yum": return `sudo yum install -y ${pkg}`;
174
+ case "pacman": return `sudo pacman -S --noconfirm ${pkg}`;
175
+ case "apk": return `sudo apk add ${pkg}`;
176
+ case "brew": return `brew install ${pkg}`;
177
+ case "choco": return `choco install ${pkg} -y`;
178
+ default: return `echo "Cannot determine package manager. Install ${pkg} manually."`;
179
+ }
180
+ }
181
+ /**
182
+ * Get the correct service management command.
183
+ */
184
+ export function getServiceCommand(action, service, platform) {
185
+ switch (platform.initSystem) {
186
+ case "systemd": return `sudo systemctl ${action} ${service}`;
187
+ case "sysvinit": return `sudo service ${service} ${action}`;
188
+ case "openrc": return `sudo rc-service ${service} ${action}`;
189
+ case "launchd": return action === "status" ? `launchctl list | grep ${service}` : `sudo launchctl ${action === "start" ? "load" : "unload"} /Library/LaunchDaemons/${service}.plist`;
190
+ default: return `sudo systemctl ${action} ${service}`;
191
+ }
192
+ }
193
+ /**
194
+ * Map common command names to package names per distro family.
195
+ */
196
+ export const COMMAND_TO_PACKAGE = {
197
+ whois: { debian: "whois", rhel: "whois", arch: "whois", alpine: "whois" },
198
+ dig: { debian: "dnsutils", rhel: "bind-utils", arch: "bind", alpine: "bind-tools" },
199
+ nslookup: { debian: "dnsutils", rhel: "bind-utils", arch: "bind", alpine: "bind-tools" },
200
+ traceroute: { debian: "traceroute", rhel: "traceroute", arch: "traceroute", alpine: "traceroute" },
201
+ rsync: { debian: "rsync", rhel: "rsync", arch: "rsync", alpine: "rsync" },
202
+ zip: { debian: "zip", rhel: "zip", arch: "zip", alpine: "zip" },
203
+ unzip: { debian: "unzip", rhel: "unzip", arch: "unzip", alpine: "unzip" },
204
+ curl: { debian: "curl", rhel: "curl", arch: "curl", alpine: "curl" },
205
+ wget: { debian: "wget", rhel: "wget", arch: "wget", alpine: "wget" },
206
+ jq: { debian: "jq", rhel: "jq", arch: "jq", alpine: "jq" },
207
+ htop: { debian: "htop", rhel: "htop", arch: "htop", alpine: "htop" },
208
+ tree: { debian: "tree", rhel: "tree", arch: "tree", alpine: "tree" },
209
+ locate: { debian: "mlocate", rhel: "mlocate", arch: "mlocate", alpine: "mlocate" },
210
+ certbot: { debian: "certbot", rhel: "certbot", arch: "certbot", alpine: "certbot" },
211
+ nc: { debian: "netcat-openbsd", rhel: "nmap-ncat", arch: "gnu-netcat", alpine: "netcat-openbsd" },
212
+ };
213
+ /**
214
+ * Get the package name for a command on the detected platform.
215
+ */
216
+ export function getPackageForCommand(command, platform) {
217
+ const mapping = COMMAND_TO_PACKAGE[command];
218
+ if (!mapping)
219
+ return command; // Default: package name = command name
220
+ return mapping[platform.distroFamily] ?? command;
221
+ }
222
+ /**
223
+ * Format platform info for display.
224
+ */
225
+ export function formatPlatform(info) {
226
+ const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", yellow: "\x1b[33m" };
227
+ const wslLabel = info.isWSL ? ` ${c.yellow}(WSL)${c.reset}` : "";
228
+ return [
229
+ `${c.bold}Platform:${c.reset}`,
230
+ ` OS: ${info.distro}${wslLabel}`,
231
+ ` Kernel: ${info.kernel}`,
232
+ ` Arch: ${info.arch}`,
233
+ ` Shell: ${info.shell}`,
234
+ ` Packages: ${info.packageManager}`,
235
+ ` Init: ${info.initSystem}`,
236
+ ].join("\n");
237
+ }
238
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
239
+ function tryExec(cmd) {
240
+ try {
241
+ return execSync(cmd, { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ function commandExists(cmd) {
248
+ return tryExec(`command -v ${cmd}`) !== null;
249
+ }
250
+ function extractField(content, key) {
251
+ const match = content.match(new RegExp(`^${key}="?([^"\\n]*)"?`, "m"));
252
+ return match?.[1] ?? null;
253
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Smart file output.
3
+ *
4
+ * - Checks file size before reading
5
+ * - Shows a sample (head + tail) if file is large (>100 lines / >50KB)
6
+ * - Searches within a file for a term and shows matches with context
7
+ * - Detects file type and syntax-highlights key parts
8
+ */
9
+ export interface FileInfo {
10
+ path: string;
11
+ exists: boolean;
12
+ sizeBytes: number;
13
+ sizeHuman: string;
14
+ lineCount: number;
15
+ isBig: boolean;
16
+ type: string;
17
+ }
18
+ /**
19
+ * Get file info (size, line count) before reading.
20
+ */
21
+ export declare function getFileInfo(path: string, isRemote: boolean, environment?: string): Promise<FileInfo>;
22
+ /**
23
+ * Read a file smartly — full content if small, sampled if large.
24
+ */
25
+ export declare function smartRead(path: string, isRemote: boolean, environment?: string): Promise<string>;
26
+ /**
27
+ * Search within a file for a term and show matches with context.
28
+ */
29
+ export declare function smartSearch(path: string, query: string, isRemote: boolean, environment?: string, contextLines?: number): Promise<string>;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Smart file output.
3
+ *
4
+ * - Checks file size before reading
5
+ * - Shows a sample (head + tail) if file is large (>100 lines / >50KB)
6
+ * - Searches within a file for a term and shows matches with context
7
+ * - Detects file type and syntax-highlights key parts
8
+ */
9
+ import { runLocalCommand } from "../execution/ssh.js";
10
+ const c = {
11
+ reset: "\x1b[0m",
12
+ bold: "\x1b[1m",
13
+ dim: "\x1b[2m",
14
+ green: "\x1b[32m",
15
+ yellow: "\x1b[33m",
16
+ red: "\x1b[31m",
17
+ cyan: "\x1b[36m",
18
+ magenta: "\x1b[35m",
19
+ };
20
+ const MAX_LINES = 80;
21
+ const MAX_BYTES = 50 * 1024; // 50KB
22
+ /**
23
+ * Get file info (size, line count) before reading.
24
+ */
25
+ export async function getFileInfo(path, isRemote, environment) {
26
+ const run = isRemote
27
+ ? async (cmd) => {
28
+ const { runRemoteCommand } = await import("../execution/ssh.js");
29
+ return runRemoteCommand(environment, cmd);
30
+ }
31
+ : runLocalCommand;
32
+ try {
33
+ const info = await run(`stat -c '%s' ${path} 2>/dev/null && wc -l < ${path} 2>/dev/null && file -b ${path} 2>/dev/null`);
34
+ const parts = info.trim().split("\n");
35
+ const sizeBytes = parseInt(parts[0]) || 0;
36
+ const lineCount = parseInt(parts[1]) || 0;
37
+ const type = parts[2] ?? "unknown";
38
+ return {
39
+ path,
40
+ exists: true,
41
+ sizeBytes,
42
+ sizeHuman: formatBytes(sizeBytes),
43
+ lineCount,
44
+ isBig: lineCount > MAX_LINES || sizeBytes > MAX_BYTES,
45
+ type,
46
+ };
47
+ }
48
+ catch {
49
+ return {
50
+ path,
51
+ exists: false,
52
+ sizeBytes: 0,
53
+ sizeHuman: "0 B",
54
+ lineCount: 0,
55
+ isBig: false,
56
+ type: "unknown",
57
+ };
58
+ }
59
+ }
60
+ /**
61
+ * Read a file smartly — full content if small, sampled if large.
62
+ */
63
+ export async function smartRead(path, isRemote, environment) {
64
+ const run = isRemote
65
+ ? async (cmd) => {
66
+ const { runRemoteCommand } = await import("../execution/ssh.js");
67
+ return runRemoteCommand(environment, cmd);
68
+ }
69
+ : runLocalCommand;
70
+ const info = await getFileInfo(path, isRemote, environment);
71
+ if (!info.exists) {
72
+ return `${c.red}File not found: ${path}${c.reset}`;
73
+ }
74
+ const lines = [];
75
+ lines.push(`${c.bold}${path}${c.reset} — ${info.sizeHuman}, ${info.lineCount} lines (${info.type})`);
76
+ lines.push("");
77
+ if (!info.isBig) {
78
+ // Small file — show everything
79
+ const content = await run(`cat ${path}`);
80
+ lines.push(numberLines(content.trimEnd()));
81
+ }
82
+ else {
83
+ // Large file — show sample
84
+ lines.push(`${c.yellow}⚠ Large file (${info.lineCount} lines, ${info.sizeHuman}). Showing sample.${c.reset}`);
85
+ lines.push("");
86
+ // Head
87
+ lines.push(`${c.cyan}── First 30 lines ──${c.reset}`);
88
+ const head = await run(`head -30 ${path}`);
89
+ lines.push(numberLines(head.trimEnd()));
90
+ // Tail
91
+ lines.push("");
92
+ lines.push(`${c.dim}... (${info.lineCount - 60} lines omitted) ...${c.reset}`);
93
+ lines.push("");
94
+ lines.push(`${c.cyan}── Last 30 lines ──${c.reset}`);
95
+ const tail = await run(`tail -30 ${path}`);
96
+ const tailStart = Math.max(1, info.lineCount - 29);
97
+ lines.push(numberLines(tail.trimEnd(), tailStart));
98
+ lines.push("");
99
+ lines.push(`${c.dim}Tip: "search <term> in ${path}" to find specific content.${c.reset}`);
100
+ }
101
+ return lines.join("\n");
102
+ }
103
+ /**
104
+ * Search within a file for a term and show matches with context.
105
+ */
106
+ export async function smartSearch(path, query, isRemote, environment, contextLines = 3) {
107
+ const run = isRemote
108
+ ? async (cmd) => {
109
+ const { runRemoteCommand } = await import("../execution/ssh.js");
110
+ return runRemoteCommand(environment, cmd);
111
+ }
112
+ : runLocalCommand;
113
+ const info = await getFileInfo(path, isRemote, environment);
114
+ if (!info.exists) {
115
+ return `${c.red}File not found: ${path}${c.reset}`;
116
+ }
117
+ const lines = [];
118
+ // Run grep with context
119
+ const safeQuery = query.replace(/['"]/g, "");
120
+ try {
121
+ const result = await run(`grep -n -i -C ${contextLines} --color=never '${safeQuery}' ${path} 2>/dev/null | head -100`);
122
+ if (!result.trim()) {
123
+ lines.push(`${c.yellow}No matches for "${query}" in ${path}${c.reset}`);
124
+ return lines.join("\n");
125
+ }
126
+ // Count total matches
127
+ const matchCount = await run(`grep -c -i '${safeQuery}' ${path} 2>/dev/null`);
128
+ const count = parseInt(matchCount.trim()) || 0;
129
+ lines.push(`${c.bold}${path}${c.reset} — ${count} match(es) for "${c.cyan}${query}${c.reset}"`);
130
+ lines.push("");
131
+ // Format the grep output — highlight matches
132
+ for (const line of result.split("\n")) {
133
+ if (line === "--") {
134
+ // Grep separator between groups
135
+ lines.push(`${c.dim} ---${c.reset}`);
136
+ continue;
137
+ }
138
+ // Line format from grep -n: "42:content" or "42-context"
139
+ const matchLine = line.match(/^(\d+)([:|-])(.*)$/);
140
+ if (matchLine) {
141
+ const lineNum = matchLine[1].padStart(5);
142
+ const isMatch = matchLine[2] === ":";
143
+ const content = matchLine[3];
144
+ if (isMatch) {
145
+ // Highlight the matched term
146
+ const highlighted = content.replace(new RegExp(`(${escapeRegex(safeQuery)})`, "gi"), `${c.bold}${c.red}$1${c.reset}`);
147
+ lines.push(` ${c.green}${lineNum}${c.reset} │ ${highlighted}`);
148
+ }
149
+ else {
150
+ lines.push(` ${c.dim}${lineNum}${c.reset} │ ${c.dim}${content}${c.reset}`);
151
+ }
152
+ }
153
+ else if (line.trim()) {
154
+ lines.push(` │ ${line}`);
155
+ }
156
+ }
157
+ if (count > 20) {
158
+ lines.push("");
159
+ lines.push(`${c.dim}Showing first matches. Total: ${count}. Narrow your search for more specific results.${c.reset}`);
160
+ }
161
+ }
162
+ catch {
163
+ lines.push(`${c.yellow}No matches for "${query}" in ${path}${c.reset}`);
164
+ }
165
+ return lines.join("\n");
166
+ }
167
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
168
+ function numberLines(content, startLine = 1) {
169
+ return content
170
+ .split("\n")
171
+ .map((line, i) => {
172
+ const num = String(startLine + i).padStart(5);
173
+ return ` ${c.dim}${num}${c.reset} │ ${line}`;
174
+ })
175
+ .join("\n");
176
+ }
177
+ function formatBytes(bytes) {
178
+ if (bytes < 1024)
179
+ return `${bytes} B`;
180
+ if (bytes < 1048576)
181
+ return `${(bytes / 1024).toFixed(1)} KB`;
182
+ if (bytes < 1073741824)
183
+ return `${(bytes / 1048576).toFixed(1)} MB`;
184
+ return `${(bytes / 1073741824).toFixed(1)} GB`;
185
+ }
186
+ function escapeRegex(str) {
187
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
188
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Terminal spinner, progress bar, and animation utilities.
3
+ * Zero external dependencies -- uses ANSI escape codes directly.
4
+ * Requires Node 18+.
5
+ */
6
+ export declare class Spinner {
7
+ private frameIndex;
8
+ private timer;
9
+ private message;
10
+ private stream;
11
+ /**
12
+ * Start the spinner with an initial message.
13
+ * If the spinner is already running it will be updated in-place.
14
+ */
15
+ start(message: string): this;
16
+ /** Change the message while the spinner keeps running. */
17
+ update(message: string): this;
18
+ /** Stop with a green checkmark and a final message. */
19
+ succeed(message?: string): void;
20
+ /** Stop with a red cross and a final message. */
21
+ fail(message?: string): void;
22
+ /** Stop the spinner and clear its line. */
23
+ stop(): void;
24
+ private render;
25
+ private clearLine;
26
+ private clearTimer;
27
+ private stopWith;
28
+ }
29
+ /**
30
+ * Convenience wrapper that starts a spinner, awaits an async function, and
31
+ * automatically calls `succeed` or `fail` depending on the outcome.
32
+ *
33
+ * @returns The resolved value of `fn`.
34
+ *
35
+ * ```ts
36
+ * const data = await withSpinner("Fetching data…", async (spinner) => {
37
+ * const res = await fetch(url);
38
+ * spinner.update("Parsing response…");
39
+ * return res.json();
40
+ * });
41
+ * ```
42
+ */
43
+ export declare function withSpinner<T>(message: string, fn: (spinner: Spinner) => Promise<T>): Promise<T>;
44
+ /**
45
+ * Returns a string representing a progress bar, e.g.:
46
+ *
47
+ * [████████░░░░░░░░░░░░] 40%
48
+ *
49
+ * @param current Current progress value (0 … total).
50
+ * @param total Value that represents 100%.
51
+ * @param width Character width of the bar (default 30).
52
+ */
53
+ export declare function progressBar(current: number, total: number, width?: number): string;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Terminal spinner, progress bar, and animation utilities.
3
+ * Zero external dependencies -- uses ANSI escape codes directly.
4
+ * Requires Node 18+.
5
+ */
6
+ // ---------------------------------------------------------------------------
7
+ // ANSI helpers
8
+ // ---------------------------------------------------------------------------
9
+ const ESC = "\x1b[";
10
+ const HIDE_CURSOR = `${ESC}?25l`;
11
+ const SHOW_CURSOR = `${ESC}?25h`;
12
+ const ERASE_LINE = `${ESC}2K`;
13
+ const MOVE_TO_COL1 = `\r`;
14
+ const RESET = `${ESC}0m`;
15
+ const CYAN = `${ESC}36m`;
16
+ const GREEN = `${ESC}32m`;
17
+ const RED = `${ESC}31m`;
18
+ // ---------------------------------------------------------------------------
19
+ // Spinner
20
+ // ---------------------------------------------------------------------------
21
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22
+ const INTERVAL_MS = 80;
23
+ export class Spinner {
24
+ frameIndex = 0;
25
+ timer = null;
26
+ message = "";
27
+ stream = process.stderr;
28
+ /**
29
+ * Start the spinner with an initial message.
30
+ * If the spinner is already running it will be updated in-place.
31
+ */
32
+ start(message) {
33
+ if (this.timer) {
34
+ this.update(message);
35
+ return this;
36
+ }
37
+ this.message = message;
38
+ this.frameIndex = 0;
39
+ this.stream.write(HIDE_CURSOR);
40
+ this.timer = setInterval(() => {
41
+ this.render();
42
+ }, INTERVAL_MS);
43
+ // Render the first frame immediately so there is no 80 ms gap.
44
+ this.render();
45
+ return this;
46
+ }
47
+ /** Change the message while the spinner keeps running. */
48
+ update(message) {
49
+ this.message = message;
50
+ return this;
51
+ }
52
+ /** Stop with a green checkmark and a final message. */
53
+ succeed(message) {
54
+ this.stopWith(`${GREEN}✔${RESET}`, message ?? this.message);
55
+ }
56
+ /** Stop with a red cross and a final message. */
57
+ fail(message) {
58
+ this.stopWith(`${RED}✖${RESET}`, message ?? this.message);
59
+ }
60
+ /** Stop the spinner and clear its line. */
61
+ stop() {
62
+ this.clearTimer();
63
+ this.clearLine();
64
+ this.stream.write(SHOW_CURSOR);
65
+ }
66
+ // -----------------------------------------------------------------------
67
+ // Private helpers
68
+ // -----------------------------------------------------------------------
69
+ render() {
70
+ const frame = FRAMES[this.frameIndex % FRAMES.length];
71
+ this.frameIndex++;
72
+ this.clearLine();
73
+ this.stream.write(`${CYAN}${frame}${RESET} ${this.message}`);
74
+ }
75
+ clearLine() {
76
+ this.stream.write(`${MOVE_TO_COL1}${ERASE_LINE}`);
77
+ }
78
+ clearTimer() {
79
+ if (this.timer) {
80
+ clearInterval(this.timer);
81
+ this.timer = null;
82
+ }
83
+ }
84
+ stopWith(symbol, message) {
85
+ this.clearTimer();
86
+ this.clearLine();
87
+ this.stream.write(`${symbol} ${message}\n`);
88
+ this.stream.write(SHOW_CURSOR);
89
+ }
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // withSpinner wrapper
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * Convenience wrapper that starts a spinner, awaits an async function, and
96
+ * automatically calls `succeed` or `fail` depending on the outcome.
97
+ *
98
+ * @returns The resolved value of `fn`.
99
+ *
100
+ * ```ts
101
+ * const data = await withSpinner("Fetching data…", async (spinner) => {
102
+ * const res = await fetch(url);
103
+ * spinner.update("Parsing response…");
104
+ * return res.json();
105
+ * });
106
+ * ```
107
+ */
108
+ export async function withSpinner(message, fn) {
109
+ const spinner = new Spinner();
110
+ spinner.start(message);
111
+ try {
112
+ const result = await fn(spinner);
113
+ spinner.succeed();
114
+ return result;
115
+ }
116
+ catch (error) {
117
+ const errMsg = error instanceof Error ? error.message : String(error);
118
+ spinner.fail(`${message} — ${errMsg}`);
119
+ throw error;
120
+ }
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Progress bar
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Returns a string representing a progress bar, e.g.:
127
+ *
128
+ * [████████░░░░░░░░░░░░] 40%
129
+ *
130
+ * @param current Current progress value (0 … total).
131
+ * @param total Value that represents 100%.
132
+ * @param width Character width of the bar (default 30).
133
+ */
134
+ export function progressBar(current, total, width = 30) {
135
+ const ratio = total === 0 ? 0 : Math.min(Math.max(current / total, 0), 1);
136
+ const filled = Math.round(ratio * width);
137
+ const empty = width - filled;
138
+ const pct = Math.round(ratio * 100);
139
+ return `[${GREEN}${"█".repeat(filled)}${RESET}${"░".repeat(empty)}] ${pct}%`;
140
+ }