unitup 0.0.11 → 0.1.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.
package/src/cli.js CHANGED
@@ -19,9 +19,20 @@ import {
19
19
  inspectService,
20
20
  getServiceFailures,
21
21
  setServiceLimits,
22
- executeJournalctlMaintenance
22
+ executeJournalctlMaintenance,
23
+ getServiceMemoryUsage,
24
+ getAllServicesMemoryUsage
23
25
  } from './systemd.js';
24
- import { sanitizeServiceName, formatTable } from './utils.js';
26
+ import {
27
+ createSchedule,
28
+ listSchedules,
29
+ getScheduleStatus,
30
+ runSchedule,
31
+ enableSchedule,
32
+ disableSchedule,
33
+ removeSchedule
34
+ } from './schedule.js';
35
+ import { sanitizeServiceName, formatTable, readGlobalConfig, saveGlobalConfig, validateMemorySize } from './utils.js';
25
36
 
26
37
  /**
27
38
  * Custom light CLI argument parser without runtime dependencies.
@@ -58,6 +69,8 @@ export function parseArgs(argv) {
58
69
  memoryMax: '',
59
70
  memorySwapMax: '',
60
71
  resetMemory: false,
72
+ defaultMemory: '',
73
+ resetDefaultMemory: false,
61
74
  since: '',
62
75
  until: '',
63
76
  priority: '',
@@ -69,7 +82,13 @@ export function parseArgs(argv) {
69
82
  time: '',
70
83
  files: '',
71
84
  yes: false,
72
- dryRun: false
85
+ dryRun: false,
86
+ every: '',
87
+ calendar: '',
88
+ onBoot: '',
89
+ onActive: '',
90
+ randomDelay: '',
91
+ persistent: false
73
92
  }
74
93
  };
75
94
 
@@ -198,6 +217,21 @@ export function parseArgs(argv) {
198
217
  } else if (arg === '--reset-memory') {
199
218
  result.flags.resetMemory = true;
200
219
  i++;
220
+ } else if (arg === '--default-memory') {
221
+ const next = argv[i + 1];
222
+ if (next && !next.startsWith('-')) {
223
+ result.flags.defaultMemory = next;
224
+ i += 2;
225
+ } else {
226
+ result.flags.defaultMemory = '1G';
227
+ i++;
228
+ }
229
+ } else if (arg.startsWith('--default-memory=')) {
230
+ result.flags.defaultMemory = arg.slice(17) || '1G';
231
+ i++;
232
+ } else if (arg === '--reset-default-memory') {
233
+ result.flags.resetDefaultMemory = true;
234
+ i++;
201
235
  } else if (arg === '--since') {
202
236
  result.flags.since = argv[i + 1] || '';
203
237
  i += 2;
@@ -255,6 +289,39 @@ export function parseArgs(argv) {
255
289
  } else if (arg === '--dry-run') {
256
290
  result.flags.dryRun = true;
257
291
  i++;
292
+ } else if (arg === '--every') {
293
+ result.flags.every = argv[i + 1] || '';
294
+ i += 2;
295
+ } else if (arg.startsWith('--every=')) {
296
+ result.flags.every = arg.slice(8);
297
+ i++;
298
+ } else if (arg === '--calendar') {
299
+ result.flags.calendar = argv[i + 1] || '';
300
+ i += 2;
301
+ } else if (arg.startsWith('--calendar=')) {
302
+ result.flags.calendar = arg.slice(11);
303
+ i++;
304
+ } else if (arg === '--on-boot') {
305
+ result.flags.onBoot = argv[i + 1] || '';
306
+ i += 2;
307
+ } else if (arg.startsWith('--on-boot=')) {
308
+ result.flags.onBoot = arg.slice(10);
309
+ i++;
310
+ } else if (arg === '--on-active') {
311
+ result.flags.onActive = argv[i + 1] || '';
312
+ i += 2;
313
+ } else if (arg.startsWith('--on-active=')) {
314
+ result.flags.onActive = arg.slice(12);
315
+ i++;
316
+ } else if (arg === '--random-delay') {
317
+ result.flags.randomDelay = argv[i + 1] || '';
318
+ i += 2;
319
+ } else if (arg.startsWith('--random-delay=')) {
320
+ result.flags.randomDelay = arg.slice(15);
321
+ i++;
322
+ } else if (arg === '--persistent') {
323
+ result.flags.persistent = true;
324
+ i++;
258
325
  } else if (!arg.startsWith('-')) {
259
326
  if (!result.command) {
260
327
  result.command = arg;
@@ -287,25 +354,39 @@ Usage:
287
354
  unitup failures List all failed services with exit code & restarts
288
355
  unitup remove <name|@group> Stop, disable and delete a service
289
356
  unitup list / unitup ls List all services (--group <group>)
290
-
291
- Add Options:
292
- --name <name> Service name (default: script/executable name)
293
- --runtime <name> Runtime (node, python, ruby, php, bun, deno, shell, go, elixir, native)
294
- --runtime-arg <val> Runtime argument (can be specified multiple times)
295
- --command <path> Custom executable command path (bypasses auto-detection)
296
- --arg <value> Argument to pass to command (can be specified multiple times)
297
- --group <group> Assign service to a group (default: default)
298
- --cwd <path> Working directory
299
- --restart <policy> Restart policy (on-failure, always, etc. Default: on-failure)
300
- --env KEY=value Environment variable (can be specified multiple times)
301
- --env-file <file> Path to environment file
302
- --start Enable and start service immediately after creation
357
+ unitup memory / unitup top Show memory usage for all apps or a specific app
358
+ unitup config Manage global configuration (--default-memory 1G)
359
+
360
+ Schedule Commands:
361
+ unitup schedule <script> [options] Create a systemd timer schedule
362
+ unitup schedules / unitup timers List all schedule timers
363
+ unitup schedule-status <name> Show detailed schedule timer status
364
+ unitup schedule-run <name> Manually run schedule service unit once
365
+ unitup schedule-enable <name> Enable and start schedule timer
366
+ unitup schedule-disable <name> Disable and stop schedule timer
367
+ unitup schedule-remove <name> Remove schedule timer and service
368
+ unitup schedule-logs <name> View logs for a scheduled task
369
+
370
+ Memory Options:
371
+ --memory-high <size> Soft memory limit (e.g. 400M, 1G)
372
+ --memory-max <size> Hard memory limit (e.g. 512M, 1G)
373
+ --default-memory [sz] Apply default memory limit (defaults to 1G)
374
+
375
+ Schedule Options:
376
+ --every <duration> Execution interval (e.g., 30s, 10m, 2h, 1d)
377
+ --calendar <expr> Calendar expression (e.g., daily, "Mon..Fri 09:00")
378
+ --on-boot <duration> Delay relative to system boot (e.g., 5m)
379
+ --on-active <duration> Delay relative to timer activation
380
+ --random-delay <dur> Random delay to prevent stampedes (e.g., 10m)
381
+ --persistent Run immediately if a scheduled run was missed
382
+ --start Enable and start timer immediately after creation
303
383
 
304
384
  Examples:
305
- unitup add server.js --runtime node
306
- unitup add worker.py --runtime python
307
- unitup add ./server --runtime native
308
- unitup add --name worker --command /usr/bin/python3 --arg worker.py --arg --port --arg 3000
385
+ unitup add server.js --runtime node --default-memory 1G
386
+ unitup config --default-memory 1G
387
+ unitup schedule cleanup.js --every 30m --default-memory --start
388
+ unitup schedule backup.py --calendar daily --random-delay 10m --start
389
+ unitup schedule report.js --calendar "Mon..Fri 09:00" --start
309
390
  `);
310
391
  }
311
392
 
@@ -417,6 +498,7 @@ export async function runCli(argv = process.argv.slice(2)) {
417
498
  memoryHigh: flags.memoryHigh,
418
499
  memoryMax: flags.memoryMax,
419
500
  memorySwapMax: flags.memorySwapMax,
501
+ defaultMemory: flags.defaultMemory,
420
502
  force: flags.force
421
503
  });
422
504
 
@@ -656,6 +738,222 @@ export async function runCli(argv = process.argv.slice(2)) {
656
738
  break;
657
739
  }
658
740
 
741
+ case 'schedule': {
742
+ const scriptArg = positionals[0];
743
+ const envObj = {};
744
+ for (const e of flags.env) {
745
+ const idx = e.indexOf('=');
746
+ if (idx !== -1) {
747
+ envObj[e.slice(0, idx)] = e.slice(idx + 1);
748
+ } else {
749
+ envObj[e] = '';
750
+ }
751
+ }
752
+
753
+ const res = await createSchedule({
754
+ name: flags.name,
755
+ script: scriptArg,
756
+ command: flags.command,
757
+ runtime: flags.runtime,
758
+ runtimeArgs: flags.runtimeArgs,
759
+ args: flags.args,
760
+ cwd: flags.cwd,
761
+ env: envObj,
762
+ envFile: flags.envFile,
763
+ group: flags.group,
764
+ memoryHigh: flags.memoryHigh,
765
+ memoryMax: flags.memoryMax,
766
+ memorySwapMax: flags.memorySwapMax,
767
+ defaultMemory: flags.defaultMemory,
768
+ every: flags.every,
769
+ calendar: flags.calendar,
770
+ onBoot: flags.onBoot,
771
+ onActive: flags.onActive,
772
+ randomDelay: flags.randomDelay,
773
+ persistent: flags.persistent,
774
+ start: flags.start || flags.enable,
775
+ enable: flags.enable
776
+ });
777
+
778
+ console.log(`✓ Schedule "${res.name}" created.`);
779
+ console.log(`Service unit: ${res.servicePath}`);
780
+ console.log(`Timer unit: ${res.timerPath}`);
781
+ if (flags.start || flags.enable) {
782
+ console.log(`✓ Timer "${res.name}" enabled and started.`);
783
+ } else {
784
+ console.log(`Run "unitup schedule-enable ${res.name}" to enable and start the timer.`);
785
+ }
786
+ break;
787
+ }
788
+
789
+ case 'timers':
790
+ case 'schedules': {
791
+ const schedules = await listSchedules(flags.group);
792
+ if (schedules.length === 0) {
793
+ if (flags.group) {
794
+ console.log(`No schedules found in group "${flags.group}".`);
795
+ } else {
796
+ console.log('No unitup schedule timers found.');
797
+ }
798
+ break;
799
+ }
800
+
801
+ const table = formatTable(schedules, [
802
+ { key: 'name', label: 'NAME' },
803
+ { key: 'group', label: 'GROUP' },
804
+ { key: 'schedule', label: 'SCHEDULE' },
805
+ { key: 'nextRun', label: 'NEXT RUN' },
806
+ { key: 'lastRun', label: 'LAST RUN' },
807
+ { key: 'status', label: 'STATUS' }
808
+ ]);
809
+ console.log(table);
810
+ break;
811
+ }
812
+
813
+ case 'timer-status':
814
+ case 'schedule-status': {
815
+ const nameArg = positionals[0];
816
+ if (!nameArg) {
817
+ throw new Error('Schedule name is required.\nExample: unitup schedule-status cleanup');
818
+ }
819
+ const status = await getScheduleStatus(nameArg);
820
+ console.log(`Name: ${status.name}`);
821
+ console.log(`Group: ${status.group}`);
822
+ console.log(`Schedule: ${status.schedule}`);
823
+ console.log(`Next Run: ${status.nextRun}`);
824
+ console.log(`Last Run: ${status.lastRun}`);
825
+ console.log(`Status: ${status.status}`);
826
+ console.log(`Timer Unit: ${status.timerUnit}`);
827
+ console.log(`Service Unit: ${status.serviceUnit}`);
828
+ break;
829
+ }
830
+
831
+ case 'schedule-run': {
832
+ const nameArg = positionals[0];
833
+ if (!nameArg) {
834
+ throw new Error('Schedule name is required.\nExample: unitup schedule-run cleanup');
835
+ }
836
+ await runSchedule(nameArg);
837
+ console.log(`✓ Service unit for schedule "${sanitizeServiceName(nameArg)}" triggered.`);
838
+ break;
839
+ }
840
+
841
+ case 'schedule-enable': {
842
+ const nameArg = positionals[0];
843
+ if (!nameArg) {
844
+ throw new Error('Schedule name is required.\nExample: unitup schedule-enable cleanup');
845
+ }
846
+ await enableSchedule(nameArg);
847
+ console.log(`✓ Schedule timer "${sanitizeServiceName(nameArg)}" enabled and started.`);
848
+ break;
849
+ }
850
+
851
+ case 'schedule-disable': {
852
+ const nameArg = positionals[0];
853
+ if (!nameArg) {
854
+ throw new Error('Schedule name is required.\nExample: unitup schedule-disable cleanup');
855
+ }
856
+ await disableSchedule(nameArg);
857
+ console.log(`✓ Schedule timer "${sanitizeServiceName(nameArg)}" disabled.`);
858
+ break;
859
+ }
860
+
861
+ case 'schedule-remove': {
862
+ const nameArg = positionals[0];
863
+ if (!nameArg) {
864
+ throw new Error('Schedule name is required.\nExample: unitup schedule-remove cleanup');
865
+ }
866
+ await removeSchedule(nameArg, { force: flags.force });
867
+ console.log(`✓ Schedule "${sanitizeServiceName(nameArg)}" removed.`);
868
+ break;
869
+ }
870
+
871
+ case 'schedule-logs': {
872
+ const nameArg = positionals[0];
873
+ if (!nameArg) {
874
+ throw new Error('Schedule name is required.\nExample: unitup schedule-logs cleanup');
875
+ }
876
+ const output = await runJournalctlLogs(nameArg, {
877
+ follow: flags.follow,
878
+ lines: flags.lines,
879
+ cat: flags.cat,
880
+ output: flags.output,
881
+ since: flags.since,
882
+ until: flags.until,
883
+ priority: flags.priority,
884
+ grep: flags.grep,
885
+ boot: flags.boot,
886
+ json: flags.json,
887
+ diskUsage: flags.diskUsage
888
+ });
889
+ if (typeof output === 'string') {
890
+ console.log(output);
891
+ }
892
+ break;
893
+ }
894
+
895
+ case 'config': {
896
+ if (flags.resetDefaultMemory) {
897
+ saveGlobalConfig({ defaultMemory: null });
898
+ console.log('✓ Default memory limit reset.');
899
+ } else if (flags.defaultMemory) {
900
+ const validSize = validateMemorySize(flags.defaultMemory, 'Default memory limit');
901
+ saveGlobalConfig({ defaultMemory: validSize });
902
+ console.log(`✓ Default memory limit set to ${validSize}.`);
903
+ } else {
904
+ const cfg = readGlobalConfig();
905
+ console.log(`Default Memory: ${cfg.defaultMemory || 'not set (1G on demand)'}`);
906
+ }
907
+ break;
908
+ }
909
+
910
+ case 'top':
911
+ case 'mem':
912
+ case 'memory': {
913
+ const nameArg = positionals[0];
914
+ if (nameArg) {
915
+ const targetNames = await resolveTargetNames(nameArg);
916
+ for (const name of targetNames) {
917
+ const mem = await getServiceMemoryUsage(name);
918
+ console.log(`=== Memory Overview: ${mem.name} ===`);
919
+ console.log(`Group: ${mem.group}`);
920
+ console.log(`Type: ${mem.type}`);
921
+ console.log(`Status: ${mem.status}`);
922
+ console.log(`PID: ${mem.pid}`);
923
+ console.log(`Current Memory: ${mem.memory}`);
924
+ console.log(`Peak Memory: ${mem.memoryPeak}`);
925
+ console.log(`Soft Limit (High): ${mem.memoryHigh}`);
926
+ console.log(`Hard Limit (Max): ${mem.memoryMax}`);
927
+ console.log(`Swap Limit: ${mem.memorySwapMax}`);
928
+ if (targetNames.length > 1) console.log('');
929
+ }
930
+ } else {
931
+ const overview = await getAllServicesMemoryUsage({ group: flags.group });
932
+ if (overview.items.length === 0) {
933
+ if (flags.group) {
934
+ console.log(`No services or schedules found in group "${flags.group}".`);
935
+ } else {
936
+ console.log('No unitup user services or schedules found.');
937
+ }
938
+ break;
939
+ }
940
+
941
+ const table = formatTable(overview.items, [
942
+ { key: 'name', label: 'NAME' },
943
+ { key: 'group', label: 'GROUP' },
944
+ { key: 'type', label: 'TYPE' },
945
+ { key: 'status', label: 'STATUS' },
946
+ { key: 'pid', label: 'PID' },
947
+ { key: 'memory', label: 'MEMORY' },
948
+ { key: 'memoryPeak', label: 'PEAK' },
949
+ { key: 'memoryMax', label: 'LIMIT (MAX)' }
950
+ ]);
951
+ console.log(table);
952
+ console.log(`\nTotal Memory Usage across ${overview.runningCount} active app(s): ${overview.totalMemory}`);
953
+ }
954
+ break;
955
+ }
956
+
659
957
  default: {
660
958
  console.error(`Unknown command: "${command}"\n`);
661
959
  printHelp();
package/src/index.js CHANGED
@@ -20,25 +20,50 @@ import {
20
20
  checkUserLinger,
21
21
  checkNodeDiagnostics,
22
22
  findNodeExecutable,
23
+ getServiceMemoryUsage,
24
+ getAllServicesMemoryUsage,
23
25
  setCommandRunner,
24
26
  resetCommandRunner
25
27
  } from './systemd.js';
26
28
  import { getDoctorInfo } from './doctor.js';
29
+ import {
30
+ createSchedule,
31
+ listSchedules,
32
+ getScheduleStatus,
33
+ runSchedule,
34
+ enableSchedule,
35
+ disableSchedule,
36
+ removeSchedule,
37
+ validateCalendar
38
+ } from './schedule.js';
27
39
  import {
28
40
  getUserUnitDir,
29
41
  getUnitPath,
42
+ getTimerPath,
30
43
  unitFileExists,
31
- parseUnitContent
44
+ timerFileExists,
45
+ parseUnitContent,
46
+ generateTimerContent,
47
+ generateScheduleServiceContent
32
48
  } from './unit.js';
33
49
  import {
34
50
  sanitizeServiceName,
35
51
  getUnitFilename,
52
+ getTimerFilename,
36
53
  getServiceNameFromUnit,
37
54
  readAppMetadata,
38
55
  getAppMetadataPath,
56
+ readScheduleMetadata,
57
+ getScheduleMetadataPath,
58
+ readGlobalConfig,
59
+ saveGlobalConfig,
60
+ getConfigFilepath,
61
+ resolveEffectiveMemoryLimits,
62
+ getSchedulesDir,
39
63
  getAppsDir,
40
64
  getUnitupDir,
41
65
  validateMemorySize,
66
+ validateDuration,
42
67
  formatMemoryBytes
43
68
  } from './utils.js';
44
69
  import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
@@ -58,20 +83,40 @@ export {
58
83
  getServiceFailures,
59
84
  getServicesByGroup,
60
85
  setServiceLimits,
86
+ getServiceMemoryUsage,
87
+ getAllServicesMemoryUsage,
61
88
  executeJournalctlMaintenance as executeJournalctlMaintenance,
62
89
  runJournalctlLogs as getServiceLogs,
63
90
 
64
- // Memory & Validation helpers
91
+ // Programmatic schedule management API
92
+ createSchedule,
93
+ listSchedules,
94
+ getScheduleStatus,
95
+ runSchedule,
96
+ enableSchedule,
97
+ disableSchedule,
98
+ removeSchedule,
99
+ validateCalendar,
100
+
101
+ // Duration & Validation helpers
65
102
  validateMemorySize,
103
+ validateDuration,
66
104
  formatMemoryBytes,
67
105
 
68
106
  // Runtime detection & resolution
69
107
  detectRuntime,
70
108
  resolveRuntimeConfig,
71
109
 
72
- // Metadata helpers
110
+ // Metadata & Config helpers
73
111
  readAppMetadata,
74
112
  getAppMetadataPath,
113
+ readScheduleMetadata,
114
+ getScheduleMetadataPath,
115
+ readGlobalConfig,
116
+ saveGlobalConfig,
117
+ getConfigFilepath,
118
+ resolveEffectiveMemoryLimits,
119
+ getSchedulesDir,
75
120
  getAppsDir,
76
121
  getUnitupDir,
77
122
 
@@ -89,10 +134,15 @@ export {
89
134
  // Unit file & path helpers
90
135
  getUserUnitDir,
91
136
  getUnitPath,
137
+ getTimerPath,
92
138
  unitFileExists,
139
+ timerFileExists,
93
140
  parseUnitContent,
141
+ generateTimerContent,
142
+ generateScheduleServiceContent,
94
143
  sanitizeServiceName,
95
144
  getUnitFilename,
145
+ getTimerFilename,
96
146
  getServiceNameFromUnit,
97
147
 
98
148
  // Runner override for testing