unitup 0.0.9 → 0.0.11

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/src/utils.js CHANGED
@@ -29,6 +29,101 @@ export function getAppMetadataPath(name) {
29
29
  return path.join(getAppsDir(), `${safeName}.json`);
30
30
  }
31
31
 
32
+ /**
33
+ * Validates systemd memory limit sizes (128K, 256M, 1G, 2T, infinity, max, or raw bytes).
34
+ * @param {string|number} val
35
+ * @param {string} paramName
36
+ * @returns {string} Normalized memory limit string
37
+ */
38
+ export function validateMemorySize(val, paramName = 'Memory limit') {
39
+ if (val === null || val === undefined || val === '') {
40
+ throw new Error(`${paramName} cannot be empty.`);
41
+ }
42
+
43
+ const str = String(val).trim();
44
+
45
+ // Prevent shell injection and unsafe characters
46
+ if (/[;&|$`"'\n\r\t ]/.test(str)) {
47
+ throw new Error(`${paramName} contains invalid characters or shell injection attempt: "${str}".`);
48
+ }
49
+
50
+ // Reject negative numbers
51
+ if (str.startsWith('-')) {
52
+ throw new Error(`${paramName} cannot be negative: "${str}".`);
53
+ }
54
+
55
+ const lower = str.toLowerCase();
56
+ if (lower === 'infinity' || lower === 'max') {
57
+ return 'infinity';
58
+ }
59
+
60
+ // Matches numbers with optional K, M, G, T, P unit suffix (with optional 'b' or 'B')
61
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*([kmgtp]?[bb]?)?$/i);
62
+ if (!match) {
63
+ throw new Error(`Invalid ${paramName} format: "${str}". Expected values like 128K, 256M, 1G, or infinity.`);
64
+ }
65
+
66
+ const num = match[1];
67
+ const unit = (match[2] || '').toUpperCase().replace(/B$/, '');
68
+
69
+ return `${num}${unit}`;
70
+ }
71
+
72
+ /**
73
+ * Formats byte values or systemd memory limit markers into human readable strings.
74
+ * @param {number|string} bytes
75
+ * @returns {string} Formatted memory string e.g. "284 MB", "infinity", "unavailable"
76
+ */
77
+ export function formatMemoryBytes(bytes) {
78
+ if (bytes === null || bytes === undefined || bytes === '' || bytes === 'unavailable' || bytes === '[unavailable]' || bytes === '-') {
79
+ return 'unavailable';
80
+ }
81
+
82
+ const str = String(bytes).trim().toLowerCase();
83
+ if (str === 'infinity' || str === 'max' || str === '18446744073709551615') {
84
+ return 'infinity';
85
+ }
86
+
87
+ const num = Number(bytes);
88
+ if (Number.isNaN(num) || num < 0) {
89
+ // If it's already a formatted string like "400M" or "400 MB"
90
+ if (/^\d+\s*[a-zA-Z]+$/.test(str)) {
91
+ const match = str.match(/^(\d+)\s*([a-zA-Z]+)$/);
92
+ if (match) {
93
+ const u = match[2].toUpperCase().replace(/B$/, '');
94
+ return `${match[1]} ${u === 'M' ? 'MB' : u === 'G' ? 'GB' : u === 'K' ? 'KB' : u}`;
95
+ }
96
+ }
97
+ return 'unavailable';
98
+ }
99
+
100
+ if (num === 0) return '0 MB';
101
+
102
+ const kb = 1024;
103
+ const mb = kb * 1024;
104
+ const gb = mb * 1024;
105
+ const tb = gb * 1024;
106
+
107
+ if (num >= tb) {
108
+ const val = num / tb;
109
+ return `${Number.isInteger(val) ? val : val.toFixed(1)} TB`;
110
+ }
111
+ if (num >= gb) {
112
+ const val = num / gb;
113
+ return `${Number.isInteger(val) ? val : val.toFixed(1)} GB`;
114
+ }
115
+ if (num >= mb) {
116
+ const val = num / mb;
117
+ return `${Number.isInteger(val) ? val : Math.round(val)} MB`;
118
+ }
119
+ if (num >= kb) {
120
+ const val = num / kb;
121
+ return `${Number.isInteger(val) ? val : Math.round(val)} KB`;
122
+ }
123
+
124
+ return `${num} B`;
125
+ }
126
+
32
127
  /**
33
128
  * Saves metadata JSON for a service.
34
129
  * @param {object} meta
@@ -41,14 +136,32 @@ export function saveAppMetadata(meta) {
41
136
  fs.mkdirSync(dir, { recursive: true });
42
137
  }
43
138
  const filePath = getAppMetadataPath(safeName);
44
- const scriptPath = resolveAbsolutePath(meta.script);
139
+
140
+ const command = meta.command ? resolveAbsolutePath(meta.command) : (meta.node ? resolveAbsolutePath(meta.node) : process.execPath);
141
+ const rawArgs = Array.isArray(meta.args) ? meta.args : (meta.script ? [meta.script] : []);
142
+ const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
143
+ const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : (args[0] ? resolveAbsolutePath(args[0]) : command);
144
+ const cwd = meta.cwd ? resolveAbsolutePath(meta.cwd) : path.dirname(scriptPath || command);
145
+
146
+ // Extract memory resources if present
147
+ const resources = meta.resources || {};
148
+ if (meta.memoryHigh) resources.memoryHigh = validateMemorySize(meta.memoryHigh, 'MemoryHigh');
149
+ if (meta.memoryMax) resources.memoryMax = validateMemorySize(meta.memoryMax, 'MemoryMax');
150
+ if (meta.memorySwapMax) resources.memorySwapMax = validateMemorySize(meta.memorySwapMax, 'MemorySwapMax');
151
+
45
152
  const payload = {
46
153
  name: safeName,
47
154
  unit: getUnitFilename(safeName),
155
+ runtime: meta.runtime || 'node',
156
+ command,
157
+ args,
158
+ cwd,
48
159
  group: meta.group || 'default',
160
+ // Optional resources section
161
+ ...(Object.keys(resources).length > 0 ? { resources } : {}),
162
+ // Backward compatibility fields
49
163
  script: scriptPath,
50
- cwd: meta.cwd ? resolveAbsolutePath(meta.cwd) : path.dirname(scriptPath),
51
- node: meta.node ? resolveAbsolutePath(meta.node) : process.execPath
164
+ node: meta.node ? resolveAbsolutePath(meta.node) : command
52
165
  };
53
166
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
54
167
  return payload;
@@ -64,7 +177,29 @@ export function readAppMetadata(name) {
64
177
  const filePath = getAppMetadataPath(name);
65
178
  if (!fs.existsSync(filePath)) return null;
66
179
  const content = fs.readFileSync(filePath, 'utf8');
67
- return JSON.parse(content);
180
+ const data = JSON.parse(content);
181
+ if (!data) return null;
182
+
183
+ // Normalization for legacy metadata format
184
+ const command = data.command || data.node || process.execPath;
185
+ const args = data.args || (data.script ? [data.script] : []);
186
+ const runtime = data.runtime || 'node';
187
+ const script = data.script || args[0] || command;
188
+ const cwd = data.cwd || (script ? path.dirname(script) : process.cwd());
189
+
190
+ return {
191
+ ...data,
192
+ name: data.name || sanitizeServiceName(name),
193
+ unit: data.unit || getUnitFilename(name),
194
+ runtime,
195
+ command,
196
+ args,
197
+ cwd,
198
+ group: data.group || 'default',
199
+ resources: data.resources || undefined,
200
+ script,
201
+ node: data.node || command
202
+ };
68
203
  } catch {
69
204
  return null;
70
205
  }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "noEmit": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "allowJs": true,
10
+ "checkJs": false
11
+ },
12
+ "include": ["index.d.ts"]
13
+ }