@appkit/llamacpp-cli 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 (136) hide show
  1. package/.versionrc.json +16 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +474 -0
  4. package/bin/llamacpp +26 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +196 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/delete.d.ts +2 -0
  10. package/dist/commands/delete.d.ts.map +1 -0
  11. package/dist/commands/delete.js +104 -0
  12. package/dist/commands/delete.js.map +1 -0
  13. package/dist/commands/list.d.ts +2 -0
  14. package/dist/commands/list.d.ts.map +1 -0
  15. package/dist/commands/list.js +37 -0
  16. package/dist/commands/list.js.map +1 -0
  17. package/dist/commands/logs.d.ts +8 -0
  18. package/dist/commands/logs.d.ts.map +1 -0
  19. package/dist/commands/logs.js +57 -0
  20. package/dist/commands/logs.js.map +1 -0
  21. package/dist/commands/ps.d.ts +2 -0
  22. package/dist/commands/ps.d.ts.map +1 -0
  23. package/dist/commands/ps.js +72 -0
  24. package/dist/commands/ps.js.map +1 -0
  25. package/dist/commands/pull.d.ts +6 -0
  26. package/dist/commands/pull.d.ts.map +1 -0
  27. package/dist/commands/pull.js +36 -0
  28. package/dist/commands/pull.js.map +1 -0
  29. package/dist/commands/rm.d.ts +2 -0
  30. package/dist/commands/rm.d.ts.map +1 -0
  31. package/dist/commands/rm.js +134 -0
  32. package/dist/commands/rm.js.map +1 -0
  33. package/dist/commands/run.d.ts +2 -0
  34. package/dist/commands/run.d.ts.map +1 -0
  35. package/dist/commands/run.js +198 -0
  36. package/dist/commands/run.js.map +1 -0
  37. package/dist/commands/search.d.ts +7 -0
  38. package/dist/commands/search.d.ts.map +1 -0
  39. package/dist/commands/search.js +93 -0
  40. package/dist/commands/search.js.map +1 -0
  41. package/dist/commands/show.d.ts +6 -0
  42. package/dist/commands/show.d.ts.map +1 -0
  43. package/dist/commands/show.js +196 -0
  44. package/dist/commands/show.js.map +1 -0
  45. package/dist/commands/start.d.ts +9 -0
  46. package/dist/commands/start.d.ts.map +1 -0
  47. package/dist/commands/start.js +150 -0
  48. package/dist/commands/start.js.map +1 -0
  49. package/dist/commands/stop.d.ts +2 -0
  50. package/dist/commands/stop.d.ts.map +1 -0
  51. package/dist/commands/stop.js +39 -0
  52. package/dist/commands/stop.js.map +1 -0
  53. package/dist/lib/config-generator.d.ts +30 -0
  54. package/dist/lib/config-generator.d.ts.map +1 -0
  55. package/dist/lib/config-generator.js +125 -0
  56. package/dist/lib/config-generator.js.map +1 -0
  57. package/dist/lib/launchctl-manager.d.ts +55 -0
  58. package/dist/lib/launchctl-manager.d.ts.map +1 -0
  59. package/dist/lib/launchctl-manager.js +227 -0
  60. package/dist/lib/launchctl-manager.js.map +1 -0
  61. package/dist/lib/model-downloader.d.ts +44 -0
  62. package/dist/lib/model-downloader.d.ts.map +1 -0
  63. package/dist/lib/model-downloader.js +248 -0
  64. package/dist/lib/model-downloader.js.map +1 -0
  65. package/dist/lib/model-scanner.d.ts +31 -0
  66. package/dist/lib/model-scanner.d.ts.map +1 -0
  67. package/dist/lib/model-scanner.js +145 -0
  68. package/dist/lib/model-scanner.js.map +1 -0
  69. package/dist/lib/model-search.d.ts +29 -0
  70. package/dist/lib/model-search.d.ts.map +1 -0
  71. package/dist/lib/model-search.js +131 -0
  72. package/dist/lib/model-search.js.map +1 -0
  73. package/dist/lib/port-manager.d.ts +26 -0
  74. package/dist/lib/port-manager.d.ts.map +1 -0
  75. package/dist/lib/port-manager.js +75 -0
  76. package/dist/lib/port-manager.js.map +1 -0
  77. package/dist/lib/state-manager.d.ts +59 -0
  78. package/dist/lib/state-manager.d.ts.map +1 -0
  79. package/dist/lib/state-manager.js +178 -0
  80. package/dist/lib/state-manager.js.map +1 -0
  81. package/dist/lib/status-checker.d.ts +28 -0
  82. package/dist/lib/status-checker.d.ts.map +1 -0
  83. package/dist/lib/status-checker.js +99 -0
  84. package/dist/lib/status-checker.js.map +1 -0
  85. package/dist/types/global-config.d.ts +16 -0
  86. package/dist/types/global-config.d.ts.map +1 -0
  87. package/dist/types/global-config.js +18 -0
  88. package/dist/types/global-config.js.map +1 -0
  89. package/dist/types/model-info.d.ts +9 -0
  90. package/dist/types/model-info.d.ts.map +1 -0
  91. package/dist/types/model-info.js +3 -0
  92. package/dist/types/model-info.js.map +1 -0
  93. package/dist/types/server-config.d.ts +27 -0
  94. package/dist/types/server-config.d.ts.map +1 -0
  95. package/dist/types/server-config.js +15 -0
  96. package/dist/types/server-config.js.map +1 -0
  97. package/dist/utils/file-utils.d.ts +49 -0
  98. package/dist/utils/file-utils.d.ts.map +1 -0
  99. package/dist/utils/file-utils.js +144 -0
  100. package/dist/utils/file-utils.js.map +1 -0
  101. package/dist/utils/format-utils.d.ts +29 -0
  102. package/dist/utils/format-utils.d.ts.map +1 -0
  103. package/dist/utils/format-utils.js +82 -0
  104. package/dist/utils/format-utils.js.map +1 -0
  105. package/dist/utils/process-utils.d.ts +27 -0
  106. package/dist/utils/process-utils.d.ts.map +1 -0
  107. package/dist/utils/process-utils.js +66 -0
  108. package/dist/utils/process-utils.js.map +1 -0
  109. package/package.json +56 -0
  110. package/src/cli.ts +195 -0
  111. package/src/commands/delete.ts +74 -0
  112. package/src/commands/list.ts +37 -0
  113. package/src/commands/logs.ts +61 -0
  114. package/src/commands/ps.ts +79 -0
  115. package/src/commands/pull.ts +40 -0
  116. package/src/commands/rm.ts +114 -0
  117. package/src/commands/run.ts +209 -0
  118. package/src/commands/search.ts +107 -0
  119. package/src/commands/show.ts +207 -0
  120. package/src/commands/start.ts +140 -0
  121. package/src/commands/stop.ts +39 -0
  122. package/src/lib/config-generator.ts +119 -0
  123. package/src/lib/launchctl-manager.ts +209 -0
  124. package/src/lib/model-downloader.ts +259 -0
  125. package/src/lib/model-scanner.ts +125 -0
  126. package/src/lib/model-search.ts +114 -0
  127. package/src/lib/port-manager.ts +80 -0
  128. package/src/lib/state-manager.ts +177 -0
  129. package/src/lib/status-checker.ts +113 -0
  130. package/src/types/global-config.ts +26 -0
  131. package/src/types/model-info.ts +8 -0
  132. package/src/types/server-config.ts +42 -0
  133. package/src/utils/file-utils.ts +106 -0
  134. package/src/utils/format-utils.ts +80 -0
  135. package/src/utils/process-utils.ts +60 -0
  136. package/tsconfig.json +20 -0
@@ -0,0 +1,177 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs/promises';
3
+ import { ServerConfig } from '../types/server-config';
4
+ import { GlobalConfig, DEFAULT_GLOBAL_CONFIG } from '../types/global-config';
5
+ import {
6
+ ensureDir,
7
+ writeJsonAtomic,
8
+ readJson,
9
+ fileExists,
10
+ getConfigDir,
11
+ getServersDir,
12
+ getLogsDir,
13
+ getGlobalConfigPath,
14
+ getModelsDir,
15
+ getLaunchAgentsDir,
16
+ } from '../utils/file-utils';
17
+
18
+ export class StateManager {
19
+ private configDir: string;
20
+ private serversDir: string;
21
+ private logsDir: string;
22
+ private globalConfigPath: string;
23
+
24
+ constructor() {
25
+ this.configDir = getConfigDir();
26
+ this.serversDir = getServersDir();
27
+ this.logsDir = getLogsDir();
28
+ this.globalConfigPath = getGlobalConfigPath();
29
+ }
30
+
31
+ /**
32
+ * Initialize config directories
33
+ */
34
+ async initialize(): Promise<void> {
35
+ await ensureDir(this.configDir);
36
+ await ensureDir(this.serversDir);
37
+ await ensureDir(this.logsDir);
38
+ await ensureDir(getLaunchAgentsDir());
39
+
40
+ // Create default global config if it doesn't exist
41
+ if (!(await fileExists(this.globalConfigPath))) {
42
+ const defaultConfig: GlobalConfig = {
43
+ ...DEFAULT_GLOBAL_CONFIG,
44
+ modelsDirectory: getModelsDir(),
45
+ };
46
+ await this.saveGlobalConfig(defaultConfig);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Load global configuration
52
+ */
53
+ async loadGlobalConfig(): Promise<GlobalConfig> {
54
+ await this.initialize();
55
+ return await readJson<GlobalConfig>(this.globalConfigPath);
56
+ }
57
+
58
+ /**
59
+ * Save global configuration
60
+ */
61
+ async saveGlobalConfig(config: GlobalConfig): Promise<void> {
62
+ await writeJsonAtomic(this.globalConfigPath, config);
63
+ }
64
+
65
+ /**
66
+ * Load a server configuration by ID
67
+ */
68
+ async loadServerConfig(id: string): Promise<ServerConfig | null> {
69
+ const configPath = path.join(this.serversDir, `${id}.json`);
70
+ if (!(await fileExists(configPath))) {
71
+ return null;
72
+ }
73
+ return await readJson<ServerConfig>(configPath);
74
+ }
75
+
76
+ /**
77
+ * Save a server configuration
78
+ */
79
+ async saveServerConfig(config: ServerConfig): Promise<void> {
80
+ const configPath = path.join(this.serversDir, `${config.id}.json`);
81
+ await writeJsonAtomic(configPath, config);
82
+ }
83
+
84
+ /**
85
+ * Delete a server configuration
86
+ */
87
+ async deleteServerConfig(id: string): Promise<void> {
88
+ const configPath = path.join(this.serversDir, `${id}.json`);
89
+ if (await fileExists(configPath)) {
90
+ await fs.unlink(configPath);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get all server configurations
96
+ */
97
+ async getAllServers(): Promise<ServerConfig[]> {
98
+ await ensureDir(this.serversDir);
99
+ const files = await fs.readdir(this.serversDir);
100
+ const configFiles = files.filter((f) => f.endsWith('.json'));
101
+
102
+ const servers: ServerConfig[] = [];
103
+ for (const file of configFiles) {
104
+ const filePath = path.join(this.serversDir, file);
105
+ try {
106
+ const config = await readJson<ServerConfig>(filePath);
107
+ servers.push(config);
108
+ } catch (error) {
109
+ console.error(`Failed to load server config ${file}:`, error);
110
+ }
111
+ }
112
+
113
+ return servers;
114
+ }
115
+
116
+ /**
117
+ * Find a server by port
118
+ */
119
+ async findServerByPort(port: number): Promise<ServerConfig | null> {
120
+ const servers = await this.getAllServers();
121
+ return servers.find((s) => s.port === port) || null;
122
+ }
123
+
124
+ /**
125
+ * Find a server by model name (fuzzy match)
126
+ */
127
+ async findServerByModelName(name: string): Promise<ServerConfig | null> {
128
+ const servers = await this.getAllServers();
129
+ const nameLower = name.toLowerCase();
130
+
131
+ // Try exact ID match first
132
+ const exactMatch = servers.find((s) => s.id === nameLower);
133
+ if (exactMatch) return exactMatch;
134
+
135
+ // Try partial match on model name or ID
136
+ const partialMatch = servers.find(
137
+ (s) =>
138
+ s.modelName.toLowerCase().includes(nameLower) ||
139
+ s.id.toLowerCase().includes(nameLower)
140
+ );
141
+ return partialMatch || null;
142
+ }
143
+
144
+ /**
145
+ * Find a server by identifier (ID, model name, or port)
146
+ */
147
+ async findServer(identifier: string): Promise<ServerConfig | null> {
148
+ // Try as port number
149
+ const port = parseInt(identifier, 10);
150
+ if (!isNaN(port)) {
151
+ const server = await this.findServerByPort(port);
152
+ if (server) return server;
153
+ }
154
+
155
+ // Try as ID or model name
156
+ return await this.findServerByModelName(identifier);
157
+ }
158
+
159
+ /**
160
+ * Check if a server exists for a given model
161
+ */
162
+ async serverExistsForModel(modelPath: string): Promise<boolean> {
163
+ const servers = await this.getAllServers();
164
+ return servers.some((s) => s.modelPath === modelPath);
165
+ }
166
+
167
+ /**
168
+ * Get all used ports
169
+ */
170
+ async getUsedPorts(): Promise<Set<number>> {
171
+ const servers = await this.getAllServers();
172
+ return new Set(servers.map((s) => s.port));
173
+ }
174
+ }
175
+
176
+ // Export singleton instance
177
+ export const stateManager = new StateManager();
@@ -0,0 +1,113 @@
1
+ import { ServerConfig, ServerStatus } from '../types/server-config';
2
+ import { launchctlManager, ServiceStatus } from './launchctl-manager';
3
+ import { isPortInUse, isProcessRunning } from '../utils/process-utils';
4
+ import { stateManager } from './state-manager';
5
+
6
+ export class StatusChecker {
7
+ /**
8
+ * Check the real-time status of a server
9
+ */
10
+ async checkServer(config: ServerConfig): Promise<ServiceStatus & { portListening: boolean }> {
11
+ // Get launchctl status
12
+ const launchStatus = await launchctlManager.getServiceStatus(config.label);
13
+
14
+ // Cross-check port
15
+ const portListening = await isPortInUse(config.port);
16
+
17
+ // Verify PID if reported
18
+ if (launchStatus.pid) {
19
+ const pidRunning = await isProcessRunning(launchStatus.pid);
20
+ if (!pidRunning) {
21
+ // PID reported but process not running
22
+ return {
23
+ ...launchStatus,
24
+ isRunning: false,
25
+ portListening,
26
+ };
27
+ }
28
+ }
29
+
30
+ return {
31
+ ...launchStatus,
32
+ portListening,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Determine server status based on checks
38
+ */
39
+ determineStatus(serviceStatus: ServiceStatus, portListening: boolean): ServerStatus {
40
+ if (serviceStatus.isRunning && portListening) {
41
+ return 'running';
42
+ }
43
+
44
+ if (!serviceStatus.isRunning && serviceStatus.exitCode && serviceStatus.exitCode !== 0) {
45
+ return 'crashed';
46
+ }
47
+
48
+ return 'stopped';
49
+ }
50
+
51
+ /**
52
+ * Update a server's status in its config
53
+ */
54
+ async updateServerStatus(config: ServerConfig): Promise<ServerConfig> {
55
+ const status = await this.checkServer(config);
56
+ const newStatus = this.determineStatus(status, status.portListening);
57
+
58
+ const updatedConfig: ServerConfig = {
59
+ ...config,
60
+ status: newStatus,
61
+ pid: status.pid || undefined,
62
+ };
63
+
64
+ // Update timestamps
65
+ if (newStatus === 'running' && config.status !== 'running') {
66
+ updatedConfig.lastStarted = new Date().toISOString();
67
+ } else if (newStatus === 'stopped' && config.status === 'running') {
68
+ updatedConfig.lastStopped = new Date().toISOString();
69
+ }
70
+
71
+ // Save updated config
72
+ await stateManager.saveServerConfig(updatedConfig);
73
+
74
+ return updatedConfig;
75
+ }
76
+
77
+ /**
78
+ * Update status for all servers
79
+ */
80
+ async updateAllServerStatuses(): Promise<ServerConfig[]> {
81
+ const servers = await stateManager.getAllServers();
82
+ const updated: ServerConfig[] = [];
83
+
84
+ for (const server of servers) {
85
+ const updatedServer = await this.updateServerStatus(server);
86
+ updated.push(updatedServer);
87
+ }
88
+
89
+ return updated;
90
+ }
91
+
92
+ /**
93
+ * Find crashed servers
94
+ */
95
+ async findCrashedServers(): Promise<ServerConfig[]> {
96
+ const servers = await stateManager.getAllServers();
97
+ const crashed: ServerConfig[] = [];
98
+
99
+ for (const server of servers) {
100
+ if (server.status === 'running') {
101
+ const status = await this.checkServer(server);
102
+ if (!status.isRunning && status.exitCode !== 0 && status.exitCode !== null) {
103
+ crashed.push(server);
104
+ }
105
+ }
106
+ }
107
+
108
+ return crashed;
109
+ }
110
+ }
111
+
112
+ // Export singleton instance
113
+ export const statusChecker = new StatusChecker();
@@ -0,0 +1,26 @@
1
+ export interface GlobalConfig {
2
+ version: string;
3
+ defaultPort: number;
4
+ modelsDirectory: string; // ~/models expanded to full path
5
+ llamaServerBinary: string; // /opt/homebrew/bin/llama-server
6
+ defaults: {
7
+ threads: number;
8
+ ctxSize: number;
9
+ gpuLayers: number;
10
+ };
11
+ }
12
+
13
+ /**
14
+ * Default global configuration
15
+ */
16
+ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
17
+ version: '1.0.0',
18
+ defaultPort: 9000,
19
+ modelsDirectory: '', // Set at runtime
20
+ llamaServerBinary: '/opt/homebrew/bin/llama-server',
21
+ defaults: {
22
+ threads: 8,
23
+ ctxSize: 8192,
24
+ gpuLayers: 60,
25
+ },
26
+ };
@@ -0,0 +1,8 @@
1
+ export interface ModelInfo {
2
+ filename: string; // Original filename
3
+ path: string; // Full absolute path
4
+ size: number; // File size in bytes
5
+ sizeFormatted: string; // Human-readable size (e.g., "1.9 GB")
6
+ modified: Date; // Last modified date
7
+ exists: boolean; // File exists and is readable
8
+ }
@@ -0,0 +1,42 @@
1
+ export type ServerStatus = 'running' | 'stopped' | 'crashed';
2
+
3
+ export interface ServerConfig {
4
+ id: string; // Sanitized model name (unique identifier)
5
+ modelPath: string; // Full path to GGUF file
6
+ modelName: string; // Display name (original filename)
7
+ port: number; // Server port
8
+
9
+ // llama-server configuration
10
+ threads: number;
11
+ ctxSize: number;
12
+ gpuLayers: number;
13
+ embeddings: boolean; // Always true
14
+ jinja: boolean; // Always true
15
+
16
+ // State tracking
17
+ status: ServerStatus;
18
+ pid?: number;
19
+ createdAt: string; // ISO timestamp
20
+ lastStarted?: string; // ISO timestamp
21
+ lastStopped?: string; // ISO timestamp
22
+
23
+ // launchctl metadata
24
+ plistPath: string; // Full path to plist file
25
+ label: string; // launchctl service label (com.llama.<id>)
26
+
27
+ // Logging
28
+ stdoutPath: string; // Path to stdout log
29
+ stderrPath: string; // Path to stderr log
30
+ }
31
+
32
+ /**
33
+ * Sanitize a model filename to create a valid server ID
34
+ * Example: "llama-3.2-3b-instruct-q4_k_m.gguf" → "llama-3-2-3b-instruct-q4-k-m"
35
+ */
36
+ export function sanitizeModelName(modelName: string): string {
37
+ return modelName
38
+ .replace(/\.gguf$/i, '') // Remove .gguf extension
39
+ .replace(/[^a-zA-Z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
40
+ .toLowerCase() // Lowercase
41
+ .replace(/^-+|-+$/g, ''); // Trim hyphens from ends
42
+ }
@@ -0,0 +1,106 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ /**
6
+ * Ensure a directory exists, creating it if necessary
7
+ */
8
+ export async function ensureDir(dirPath: string): Promise<void> {
9
+ try {
10
+ await fs.mkdir(dirPath, { recursive: true, mode: 0o755 });
11
+ } catch (error) {
12
+ // Ignore error if directory already exists
13
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
14
+ throw error;
15
+ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Write a file atomically (write to temp, then rename)
21
+ */
22
+ export async function writeFileAtomic(filePath: string, content: string): Promise<void> {
23
+ const tempPath = `${filePath}.tmp`;
24
+ await fs.writeFile(tempPath, content, 'utf-8');
25
+ await fs.rename(tempPath, filePath);
26
+ }
27
+
28
+ /**
29
+ * Write JSON to a file atomically
30
+ */
31
+ export async function writeJsonAtomic(filePath: string, data: any): Promise<void> {
32
+ const content = JSON.stringify(data, null, 2);
33
+ await writeFileAtomic(filePath, content);
34
+ }
35
+
36
+ /**
37
+ * Read and parse JSON file
38
+ */
39
+ export async function readJson<T>(filePath: string): Promise<T> {
40
+ const content = await fs.readFile(filePath, 'utf-8');
41
+ return JSON.parse(content);
42
+ }
43
+
44
+ /**
45
+ * Check if a file exists
46
+ */
47
+ export async function fileExists(filePath: string): Promise<boolean> {
48
+ try {
49
+ await fs.access(filePath);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get the llamacpp config directory (~/.llamacpp)
58
+ */
59
+ export function getConfigDir(): string {
60
+ return path.join(os.homedir(), '.llamacpp');
61
+ }
62
+
63
+ /**
64
+ * Get the servers directory (~/.llamacpp/servers)
65
+ */
66
+ export function getServersDir(): string {
67
+ return path.join(getConfigDir(), 'servers');
68
+ }
69
+
70
+ /**
71
+ * Get the logs directory (~/.llamacpp/logs)
72
+ */
73
+ export function getLogsDir(): string {
74
+ return path.join(getConfigDir(), 'logs');
75
+ }
76
+
77
+ /**
78
+ * Get the global config file path
79
+ */
80
+ export function getGlobalConfigPath(): string {
81
+ return path.join(getConfigDir(), 'config.json');
82
+ }
83
+
84
+ /**
85
+ * Get the models directory (~/models)
86
+ */
87
+ export function getModelsDir(): string {
88
+ return path.join(os.homedir(), 'models');
89
+ }
90
+
91
+ /**
92
+ * Get the LaunchAgents directory
93
+ */
94
+ export function getLaunchAgentsDir(): string {
95
+ return path.join(os.homedir(), 'Library', 'LaunchAgents');
96
+ }
97
+
98
+ /**
99
+ * Expand tilde (~) in path to home directory
100
+ */
101
+ export function expandHome(filePath: string): string {
102
+ if (filePath.startsWith('~/')) {
103
+ return path.join(os.homedir(), filePath.slice(2));
104
+ }
105
+ return filePath;
106
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Format bytes to human-readable size
3
+ * Example: 1900000000 → "1.9 GB"
4
+ */
5
+ export function formatBytes(bytes: number): string {
6
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
7
+ let size = bytes;
8
+ let unitIndex = 0;
9
+
10
+ while (size >= 1024 && unitIndex < units.length - 1) {
11
+ size /= 1024;
12
+ unitIndex++;
13
+ }
14
+
15
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
16
+ }
17
+
18
+ /**
19
+ * Format a date to a human-readable string
20
+ * Example: "Nov 20, 2025 10:30 AM"
21
+ */
22
+ export function formatDate(date: Date | string): string {
23
+ const d = typeof date === 'string' ? new Date(date) : date;
24
+ return d.toLocaleString('en-US', {
25
+ month: 'short',
26
+ day: 'numeric',
27
+ year: 'numeric',
28
+ hour: 'numeric',
29
+ minute: '2-digit',
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Format a date to a short string (no time)
35
+ * Example: "Nov 20"
36
+ */
37
+ export function formatDateShort(date: Date | string): string {
38
+ const d = typeof date === 'string' ? new Date(date) : date;
39
+ return d.toLocaleString('en-US', {
40
+ month: 'short',
41
+ day: 'numeric',
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Calculate uptime from start time
47
+ * Example: "2d 4h" or "45m" or "30s"
48
+ */
49
+ export function formatUptime(startTime: string): string {
50
+ const start = new Date(startTime).getTime();
51
+ const now = Date.now();
52
+ const seconds = Math.floor((now - start) / 1000);
53
+
54
+ if (seconds < 60) return `${seconds}s`;
55
+
56
+ const minutes = Math.floor(seconds / 60);
57
+ if (minutes < 60) return `${minutes}m`;
58
+
59
+ const hours = Math.floor(minutes / 60);
60
+ if (hours < 24) return `${hours}h`;
61
+
62
+ const days = Math.floor(hours / 24);
63
+ const remainingHours = hours % 24;
64
+ return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
65
+ }
66
+
67
+ /**
68
+ * Truncate a string to a maximum length
69
+ */
70
+ export function truncate(str: string, maxLength: number): string {
71
+ if (str.length <= maxLength) return str;
72
+ return str.slice(0, maxLength - 3) + '...';
73
+ }
74
+
75
+ /**
76
+ * Pad a string to a specific length
77
+ */
78
+ export function pad(str: string, length: number, char = ' '): string {
79
+ return str.padEnd(length, char);
80
+ }
@@ -0,0 +1,60 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ export const execAsync = promisify(exec);
5
+
6
+ /**
7
+ * Execute a command and return stdout
8
+ * Throws on non-zero exit code
9
+ */
10
+ export async function execCommand(command: string): Promise<string> {
11
+ const { stdout } = await execAsync(command);
12
+ return stdout.trim();
13
+ }
14
+
15
+ /**
16
+ * Execute a command and return both stdout and stderr
17
+ */
18
+ export async function execCommandFull(command: string): Promise<{ stdout: string; stderr: string }> {
19
+ const { stdout, stderr } = await execAsync(command);
20
+ return {
21
+ stdout: stdout.trim(),
22
+ stderr: stderr.trim(),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Check if a command exists in PATH
28
+ */
29
+ export async function commandExists(command: string): Promise<boolean> {
30
+ try {
31
+ await execAsync(`which ${command}`);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Check if a process is running by PID
40
+ */
41
+ export async function isProcessRunning(pid: number): Promise<boolean> {
42
+ try {
43
+ await execAsync(`ps -p ${pid}`);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check if a port is in use
52
+ */
53
+ export async function isPortInUse(port: number): Promise<boolean> {
54
+ try {
55
+ await execAsync(`lsof -iTCP:${port} -sTCP:LISTEN -t`);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }