@node-cli/timer 2.1.2 → 2.2.1

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/README.md CHANGED
@@ -27,9 +27,37 @@ Start a timer for 1 minute and 42 seconds
27
27
  Get help
28
28
 
29
29
  ```sh
30
- > search --help
30
+ > timer --help
31
31
  ```
32
32
 
33
+ ## Notifications
34
+
35
+ When the timer is done it raises a desktop notification. On macOS this is a
36
+ dialog carrying the timer's own icon, which stays on screen until you dismiss
37
+ it — a banner auto-dismisses after a few seconds, so a timer that fires while
38
+ you are away from the keyboard would leave nothing behind. The dialog is
39
+ released detached, so the timer itself exits immediately and never blocks
40
+ whatever you chained behind it.
41
+
42
+ Prefer the passive banner:
43
+
44
+ ```sh
45
+ > timer 1m42s --banner
46
+ ```
47
+
48
+ Turn notifications off entirely:
49
+
50
+ ```sh
51
+ > timer 1m42s --no-notification
52
+ ```
53
+
54
+ `--banner` is macOS-only; Linux (`notify-send`) and Windows (toast) always use
55
+ their native banner.
56
+
57
+ Be aware that macOS silently suppresses banners while a Focus mode is on or
58
+ while you are sharing your screen — `--banner` will appear to do nothing. The
59
+ dialog is an ordinary window, so it is not affected by either.
60
+
33
61
  ## License
34
62
 
35
63
  MIT © Arno Versini
Binary file
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#4F8DF7" />
5
+ <stop offset="1" stop-color="#1D4ED8" />
6
+ </linearGradient>
7
+ <linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
8
+ <stop offset="0" stop-color="#FDE68A" />
9
+ <stop offset="1" stop-color="#F59E0B" />
10
+ </linearGradient>
11
+ </defs>
12
+
13
+ <rect width="1024" height="1024" rx="228" ry="228" fill="url(#bg)" />
14
+
15
+ <g fill="none" stroke="#FFFFFF" stroke-width="34" stroke-linejoin="round" stroke-linecap="round">
16
+ <path d="M330 280 L694 280 L512 512 Z" />
17
+ <path d="M512 512 L694 744 L330 744 Z" />
18
+ </g>
19
+
20
+ <g fill="url(#sand)">
21
+ <path d="M448 430 L576 430 L512 512 Z" />
22
+ <path d="M404 650 L620 650 L694 744 L330 744 Z" />
23
+ <rect x="500" y="512" width="24" height="140" />
24
+ </g>
25
+
26
+ <g fill="#FFFFFF">
27
+ <rect x="288" y="232" width="448" height="52" rx="26" ry="26" />
28
+ <rect x="288" y="740" width="448" height="52" rx="26" ry="26" />
29
+ </g>
30
+ </svg>
@@ -1,4 +1,5 @@
1
1
  export declare const defaultFlags: {
2
+ banner: boolean;
2
3
  boring: boolean;
3
4
  notification: boolean;
4
5
  };
package/dist/defaults.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /* v8 ignore start */ export const defaultFlags = {
2
+ banner: false,
2
3
  boring: false,
3
4
  notification: true
4
5
  }; /* v8 ignore stop */
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["/* v8 ignore start */\nexport const defaultFlags = {\n\tboring: false,\n\tnotification: true,\n};\n/* v8 ignore stop */\n"],"names":["defaultFlags","boring","notification"],"mappings":"AAAA,mBAAmB,GACnB,OAAO,MAAMA,eAAe;IAC3BC,QAAQ;IACRC,cAAc;AACf,EAAE,CACF,kBAAkB"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["/* v8 ignore start */\nexport const defaultFlags = {\n\tbanner: false,\n\tboring: false,\n\tnotification: true,\n};\n/* v8 ignore stop */\n"],"names":["defaultFlags","banner","boring","notification"],"mappings":"AAAA,mBAAmB,GACnB,OAAO,MAAMA,eAAe;IAC3BC,QAAQ;IACRC,QAAQ;IACRC,cAAc;AACf,EAAE,CACF,kBAAkB"}
package/dist/notify.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type Notification = {
2
+ banner?: boolean;
2
3
  message: string;
3
4
  sound?: string;
4
5
  title: string;
@@ -6,6 +7,7 @@ export type Notification = {
6
7
  export type NotifyCommand = {
7
8
  args: string[];
8
9
  command: string;
10
+ detached: boolean;
9
11
  };
10
12
  /**
11
13
  * Builds the platform-native command used to display a desktop notification.
@@ -14,6 +16,10 @@ export type NotifyCommand = {
14
16
  * Every supported platform ships its notifier as part of the OS, so nothing is
15
17
  * vendored and nothing has to match the host CPU architecture.
16
18
  *
19
+ * Only macOS distinguishes an alert from a banner: it is the platform whose
20
+ * banner cannot carry our icon, and the one where the dialog is available for
21
+ * free. Linux and Windows always get their native banner.
22
+ *
17
23
  */
18
24
  export declare const buildNotifyCommand: (platform: string, notification: Notification) => NotifyCommand | null;
19
25
  /**
package/dist/notify.js CHANGED
@@ -1,4 +1,5 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { promisify } from "node:util";
3
4
  const execFileAsync = promisify(execFile);
4
5
  /**
@@ -6,6 +7,11 @@ const execFileAsync = promisify(execFile);
6
7
  * it is killed outright rather than merely asked to stop: a wedged child can
7
8
  * ignore SIGTERM.
8
9
  */ const NOTIFIER_TIMEOUT_MS = 5000;
10
+ /**
11
+ * Resolved from this module rather than the process, so it survives being
12
+ * invoked through a symlinked bin. `src` and `dist` sit at the same depth, so
13
+ * the same relative path works whether the source or the build is running.
14
+ */ const ALERT_ICON_PATH = fileURLToPath(new URL("../assets/hourglass.png", import.meta.url));
9
15
  /**
10
16
  * Escapes a value so it can be embedded in an AppleScript double-quoted string
11
17
  * literal, where only backslashes and double quotes are special.
@@ -14,7 +20,7 @@ const execFileAsync = promisify(execFile);
14
20
  * Escapes a value so it can be embedded in a PowerShell single-quoted string
15
21
  * literal, where a single quote is escaped by doubling it.
16
22
  */ const escapePowerShell = (value)=>value.replaceAll("'", "''");
17
- const macCommand = ({ message, sound, title })=>{
23
+ const macBannerCommand = ({ message, sound, title })=>{
18
24
  const script = [
19
25
  `display notification "${escapeAppleScript(message)}"`,
20
26
  `with title "${escapeAppleScript(title)}"`,
@@ -25,7 +31,41 @@ const macCommand = ({ message, sound, title })=>{
25
31
  "-e",
26
32
  script
27
33
  ],
28
- command: "osascript"
34
+ command: "osascript",
35
+ detached: false
36
+ };
37
+ };
38
+ /**
39
+ * A banner auto-dismisses after a few seconds, so a timer that fires while you
40
+ * are away from the keyboard leaves nothing behind. This dialog waits until it
41
+ * is acknowledged, which is the behaviour an alarm actually wants.
42
+ *
43
+ * `display dialog` takes an arbitrary image, which is the only way to escape
44
+ * the Script Editor icon macOS otherwise stamps on anything osascript posts.
45
+ *
46
+ */ const macAlertCommand = ({ message, sound, title })=>{
47
+ const dialog = [
48
+ `display dialog "${escapeAppleScript(message)}"`,
49
+ `with title "${escapeAppleScript(title)}"`,
50
+ `with icon (POSIX file "${escapeAppleScript(ALERT_ICON_PATH)}")`,
51
+ 'buttons {"OK"} default button "OK"'
52
+ ].join(" ");
53
+ /**
54
+ * `display dialog` has no sound parameter, so the alert sound is played
55
+ * alongside it. Backgrounded and silenced because a missing sound file must
56
+ * not stall or fail the dialog.
57
+ */ const play = sound ? [
58
+ "-e",
59
+ `do shell script "afplay /System/Library/Sounds/${escapeAppleScript(sound)}.aiff > /dev/null 2>&1 &"`
60
+ ] : [];
61
+ return {
62
+ args: [
63
+ ...play,
64
+ "-e",
65
+ dialog
66
+ ],
67
+ command: "osascript",
68
+ detached: true
29
69
  };
30
70
  };
31
71
  const linuxCommand = ({ message, title })=>({
@@ -33,7 +73,8 @@ const linuxCommand = ({ message, title })=>({
33
73
  title,
34
74
  message
35
75
  ],
36
- command: "notify-send"
76
+ command: "notify-send",
77
+ detached: false
37
78
  });
38
79
  const windowsCommand = ({ message, title })=>{
39
80
  const script = [
@@ -49,7 +90,8 @@ const windowsCommand = ({ message, title })=>{
49
90
  "-Command",
50
91
  script
51
92
  ],
52
- command: "powershell"
93
+ command: "powershell",
94
+ detached: false
53
95
  };
54
96
  };
55
97
  /**
@@ -59,11 +101,15 @@ const windowsCommand = ({ message, title })=>{
59
101
  * Every supported platform ships its notifier as part of the OS, so nothing is
60
102
  * vendored and nothing has to match the host CPU architecture.
61
103
  *
104
+ * Only macOS distinguishes an alert from a banner: it is the platform whose
105
+ * banner cannot carry our icon, and the one where the dialog is available for
106
+ * free. Linux and Windows always get their native banner.
107
+ *
62
108
  */ export const buildNotifyCommand = (platform, notification)=>{
63
109
  switch(platform){
64
110
  case "darwin":
65
111
  {
66
- return macCommand(notification);
112
+ return notification.banner ? macBannerCommand(notification) : macAlertCommand(notification);
67
113
  }
68
114
  case "linux":
69
115
  {
@@ -79,6 +125,27 @@ const windowsCommand = ({ message, title })=>{
79
125
  }
80
126
  }
81
127
  };
128
+ /**
129
+ * Releases a notifier that must outlive the CLI. An alert waits for a human, so
130
+ * awaiting it would hold the timer open until the dialog is clicked and stall
131
+ * anything chained behind it.
132
+ *
133
+ * Detaching costs the delivery result: spawn reports failure asynchronously,
134
+ * long after the CLI is gone. The caller is told only that the alert was
135
+ * released, which is all a best-effort courtesy needs to report.
136
+ *
137
+ */ const release = ({ args, command })=>{
138
+ const child = spawn(command, args, {
139
+ detached: true,
140
+ stdio: "ignore"
141
+ });
142
+ /**
143
+ * An unhandled "error" event on a child process is thrown, so a notifier that
144
+ * cannot spawn at all would take the timer down with it.
145
+ */ child.on("error", ()=>{});
146
+ child.unref();
147
+ return true;
148
+ };
82
149
  /**
83
150
  * Displays a desktop notification, resolving to true when it was shown.
84
151
  *
@@ -92,6 +159,9 @@ const windowsCommand = ({ message, title })=>{
92
159
  if (notifyCommand === null) {
93
160
  return false;
94
161
  }
162
+ if (notifyCommand.detached) {
163
+ return release(notifyCommand);
164
+ }
95
165
  await execFileAsync(notifyCommand.command, notifyCommand.args, {
96
166
  killSignal: "SIGKILL",
97
167
  timeout: NOTIFIER_TIMEOUT_MS
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notify.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * A notifier that spawns but never exits would keep the CLI alive forever, so\n * it is killed outright rather than merely asked to stop: a wedged child can\n * ignore SIGTERM.\n */\nconst NOTIFIER_TIMEOUT_MS = 5000;\n\nexport type Notification = {\n\tmessage: string;\n\tsound?: string;\n\ttitle: string;\n};\n\nexport type NotifyCommand = {\n\targs: string[];\n\tcommand: string;\n};\n\n/**\n * Escapes a value so it can be embedded in an AppleScript double-quoted string\n * literal, where only backslashes and double quotes are special.\n */\nconst escapeAppleScript = (value: string): string =>\n\tvalue.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n\n/**\n * Escapes a value so it can be embedded in a PowerShell single-quoted string\n * literal, where a single quote is escaped by doubling it.\n */\nconst escapePowerShell = (value: string): string => value.replaceAll(\"'\", \"''\");\n\nconst macCommand = ({ message, sound, title }: Notification): NotifyCommand => {\n\tconst script = [\n\t\t`display notification \"${escapeAppleScript(message)}\"`,\n\t\t`with title \"${escapeAppleScript(title)}\"`,\n\t\tsound ? `sound name \"${escapeAppleScript(sound)}\"` : \"\",\n\t]\n\t\t.filter(Boolean)\n\t\t.join(\" \");\n\n\treturn { args: [\"-e\", script], command: \"osascript\" };\n};\n\nconst linuxCommand = ({ message, title }: Notification): NotifyCommand => ({\n\targs: [title, message],\n\tcommand: \"notify-send\",\n});\n\nconst windowsCommand = ({ message, title }: Notification): NotifyCommand => {\n\tconst script = [\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null\",\n\t\t\"$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)\",\n\t\t`$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${escapePowerShell(title)}')) > $null`,\n\t\t`$xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${escapePowerShell(message)}')) > $null`,\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('@node-cli/timer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))\",\n\t].join(\"; \");\n\n\treturn { args: [\"-NoProfile\", \"-Command\", script], command: \"powershell\" };\n};\n\n/**\n * Builds the platform-native command used to display a desktop notification.\n * Returns null on platforms we do not know how to notify on.\n *\n * Every supported platform ships its notifier as part of the OS, so nothing is\n * vendored and nothing has to match the host CPU architecture.\n *\n */\nexport const buildNotifyCommand = (\n\tplatform: string,\n\tnotification: Notification,\n): NotifyCommand | null => {\n\tswitch (platform) {\n\t\tcase \"darwin\": {\n\t\t\treturn macCommand(notification);\n\t\t}\n\t\tcase \"linux\": {\n\t\t\treturn linuxCommand(notification);\n\t\t}\n\t\tcase \"win32\": {\n\t\t\treturn windowsCommand(notification);\n\t\t}\n\t\tdefault: {\n\t\t\treturn null;\n\t\t}\n\t}\n};\n\n/**\n * Displays a desktop notification, resolving to true when it was shown.\n *\n * A notification is a courtesy, never a reason to fail: an unsupported\n * platform, a missing notifier, or a notifier that refuses to run all resolve\n * to false instead of throwing.\n *\n */\nexport const notify = async (\n\tnotification: Notification,\n\tplatform: string = process.platform,\n): Promise<boolean> => {\n\ttry {\n\t\tconst notifyCommand = buildNotifyCommand(platform, notification);\n\n\t\tif (notifyCommand === null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait execFileAsync(notifyCommand.command, notifyCommand.args, {\n\t\t\tkillSignal: \"SIGKILL\",\n\t\t\ttimeout: NOTIFIER_TIMEOUT_MS,\n\t\t});\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n"],"names":["execFile","promisify","execFileAsync","NOTIFIER_TIMEOUT_MS","escapeAppleScript","value","replaceAll","escapePowerShell","macCommand","message","sound","title","script","filter","Boolean","join","args","command","linuxCommand","windowsCommand","buildNotifyCommand","platform","notification","notify","process","notifyCommand","killSignal","timeout"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,qBAAqB;AAC9C,SAASC,SAAS,QAAQ,YAAY;AAEtC,MAAMC,gBAAgBD,UAAUD;AAEhC;;;;CAIC,GACD,MAAMG,sBAAsB;AAa5B;;;CAGC,GACD,MAAMC,oBAAoB,CAACC,QAC1BA,MAAMC,UAAU,CAAC,MAAM,QAAQA,UAAU,CAAC,KAAK;AAEhD;;;CAGC,GACD,MAAMC,mBAAmB,CAACF,QAA0BA,MAAMC,UAAU,CAAC,KAAK;AAE1E,MAAME,aAAa,CAAC,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,EAAgB;IAC1D,MAAMC,SAAS;QACd,CAAC,sBAAsB,EAAER,kBAAkBK,SAAS,CAAC,CAAC;QACtD,CAAC,YAAY,EAAEL,kBAAkBO,OAAO,CAAC,CAAC;QAC1CD,QAAQ,CAAC,YAAY,EAAEN,kBAAkBM,OAAO,CAAC,CAAC,GAAG;KACrD,CACCG,MAAM,CAACC,SACPC,IAAI,CAAC;IAEP,OAAO;QAAEC,MAAM;YAAC;YAAMJ;SAAO;QAAEK,SAAS;IAAY;AACrD;AAEA,MAAMC,eAAe,CAAC,EAAET,OAAO,EAAEE,KAAK,EAAgB,GAAqB,CAAA;QAC1EK,MAAM;YAACL;YAAOF;SAAQ;QACtBQ,SAAS;IACV,CAAA;AAEA,MAAME,iBAAiB,CAAC,EAAEV,OAAO,EAAEE,KAAK,EAAgB;IACvD,MAAMC,SAAS;QACd;QACA;QACA,CAAC,sEAAsE,EAAEL,iBAAiBI,OAAO,WAAW,CAAC;QAC7G,CAAC,sEAAsE,EAAEJ,iBAAiBE,SAAS,WAAW,CAAC;QAC/G;KACA,CAACM,IAAI,CAAC;IAEP,OAAO;QAAEC,MAAM;YAAC;YAAc;YAAYJ;SAAO;QAAEK,SAAS;IAAa;AAC1E;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMG,qBAAqB,CACjCC,UACAC;IAEA,OAAQD;QACP,KAAK;YAAU;gBACd,OAAOb,WAAWc;YACnB;QACA,KAAK;YAAS;gBACb,OAAOJ,aAAaI;YACrB;QACA,KAAK;YAAS;gBACb,OAAOH,eAAeG;YACvB;QACA;YAAS;gBACR,OAAO;YACR;IACD;AACD,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMC,SAAS,OACrBD,cACAD,WAAmBG,QAAQH,QAAQ;IAEnC,IAAI;QACH,MAAMI,gBAAgBL,mBAAmBC,UAAUC;QAEnD,IAAIG,kBAAkB,MAAM;YAC3B,OAAO;QACR;QAEA,MAAMvB,cAAcuB,cAAcR,OAAO,EAAEQ,cAAcT,IAAI,EAAE;YAC9DU,YAAY;YACZC,SAASxB;QACV;QACA,OAAO;IACR,EAAE,OAAM;QACP,OAAO;IACR;AACD,EAAE"}
1
+ {"version":3,"sources":["../src/notify.ts"],"sourcesContent":["import { execFile, spawn } from \"node:child_process\";\nimport { fileURLToPath } from \"node:url\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * A notifier that spawns but never exits would keep the CLI alive forever, so\n * it is killed outright rather than merely asked to stop: a wedged child can\n * ignore SIGTERM.\n */\nconst NOTIFIER_TIMEOUT_MS = 5000;\n\n/**\n * Resolved from this module rather than the process, so it survives being\n * invoked through a symlinked bin. `src` and `dist` sit at the same depth, so\n * the same relative path works whether the source or the build is running.\n */\nconst ALERT_ICON_PATH = fileURLToPath(\n\tnew URL(\"../assets/hourglass.png\", import.meta.url),\n);\n\nexport type Notification = {\n\tbanner?: boolean;\n\tmessage: string;\n\tsound?: string;\n\ttitle: string;\n};\n\nexport type NotifyCommand = {\n\targs: string[];\n\tcommand: string;\n\tdetached: boolean;\n};\n\n/**\n * Escapes a value so it can be embedded in an AppleScript double-quoted string\n * literal, where only backslashes and double quotes are special.\n */\nconst escapeAppleScript = (value: string): string =>\n\tvalue.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n\n/**\n * Escapes a value so it can be embedded in a PowerShell single-quoted string\n * literal, where a single quote is escaped by doubling it.\n */\nconst escapePowerShell = (value: string): string => value.replaceAll(\"'\", \"''\");\n\nconst macBannerCommand = ({\n\tmessage,\n\tsound,\n\ttitle,\n}: Notification): NotifyCommand => {\n\tconst script = [\n\t\t`display notification \"${escapeAppleScript(message)}\"`,\n\t\t`with title \"${escapeAppleScript(title)}\"`,\n\t\tsound ? `sound name \"${escapeAppleScript(sound)}\"` : \"\",\n\t]\n\t\t.filter(Boolean)\n\t\t.join(\" \");\n\n\treturn { args: [\"-e\", script], command: \"osascript\", detached: false };\n};\n\n/**\n * A banner auto-dismisses after a few seconds, so a timer that fires while you\n * are away from the keyboard leaves nothing behind. This dialog waits until it\n * is acknowledged, which is the behaviour an alarm actually wants.\n *\n * `display dialog` takes an arbitrary image, which is the only way to escape\n * the Script Editor icon macOS otherwise stamps on anything osascript posts.\n *\n */\nconst macAlertCommand = ({\n\tmessage,\n\tsound,\n\ttitle,\n}: Notification): NotifyCommand => {\n\tconst dialog = [\n\t\t`display dialog \"${escapeAppleScript(message)}\"`,\n\t\t`with title \"${escapeAppleScript(title)}\"`,\n\t\t`with icon (POSIX file \"${escapeAppleScript(ALERT_ICON_PATH)}\")`,\n\t\t'buttons {\"OK\"} default button \"OK\"',\n\t].join(\" \");\n\n\t/**\n\t * `display dialog` has no sound parameter, so the alert sound is played\n\t * alongside it. Backgrounded and silenced because a missing sound file must\n\t * not stall or fail the dialog.\n\t */\n\tconst play = sound\n\t\t? [\n\t\t\t\t\"-e\",\n\t\t\t\t`do shell script \"afplay /System/Library/Sounds/${escapeAppleScript(sound)}.aiff > /dev/null 2>&1 &\"`,\n\t\t\t]\n\t\t: [];\n\n\treturn {\n\t\targs: [...play, \"-e\", dialog],\n\t\tcommand: \"osascript\",\n\t\tdetached: true,\n\t};\n};\n\nconst linuxCommand = ({ message, title }: Notification): NotifyCommand => ({\n\targs: [title, message],\n\tcommand: \"notify-send\",\n\tdetached: false,\n});\n\nconst windowsCommand = ({ message, title }: Notification): NotifyCommand => {\n\tconst script = [\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null\",\n\t\t\"$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)\",\n\t\t`$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${escapePowerShell(title)}')) > $null`,\n\t\t`$xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${escapePowerShell(message)}')) > $null`,\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('@node-cli/timer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))\",\n\t].join(\"; \");\n\n\treturn {\n\t\targs: [\"-NoProfile\", \"-Command\", script],\n\t\tcommand: \"powershell\",\n\t\tdetached: false,\n\t};\n};\n\n/**\n * Builds the platform-native command used to display a desktop notification.\n * Returns null on platforms we do not know how to notify on.\n *\n * Every supported platform ships its notifier as part of the OS, so nothing is\n * vendored and nothing has to match the host CPU architecture.\n *\n * Only macOS distinguishes an alert from a banner: it is the platform whose\n * banner cannot carry our icon, and the one where the dialog is available for\n * free. Linux and Windows always get their native banner.\n *\n */\nexport const buildNotifyCommand = (\n\tplatform: string,\n\tnotification: Notification,\n): NotifyCommand | null => {\n\tswitch (platform) {\n\t\tcase \"darwin\": {\n\t\t\treturn notification.banner\n\t\t\t\t? macBannerCommand(notification)\n\t\t\t\t: macAlertCommand(notification);\n\t\t}\n\t\tcase \"linux\": {\n\t\t\treturn linuxCommand(notification);\n\t\t}\n\t\tcase \"win32\": {\n\t\t\treturn windowsCommand(notification);\n\t\t}\n\t\tdefault: {\n\t\t\treturn null;\n\t\t}\n\t}\n};\n\n/**\n * Releases a notifier that must outlive the CLI. An alert waits for a human, so\n * awaiting it would hold the timer open until the dialog is clicked and stall\n * anything chained behind it.\n *\n * Detaching costs the delivery result: spawn reports failure asynchronously,\n * long after the CLI is gone. The caller is told only that the alert was\n * released, which is all a best-effort courtesy needs to report.\n *\n */\nconst release = ({ args, command }: NotifyCommand): boolean => {\n\tconst child = spawn(command, args, { detached: true, stdio: \"ignore\" });\n\n\t/**\n\t * An unhandled \"error\" event on a child process is thrown, so a notifier that\n\t * cannot spawn at all would take the timer down with it.\n\t */\n\tchild.on(\"error\", () => {});\n\tchild.unref();\n\treturn true;\n};\n\n/**\n * Displays a desktop notification, resolving to true when it was shown.\n *\n * A notification is a courtesy, never a reason to fail: an unsupported\n * platform, a missing notifier, or a notifier that refuses to run all resolve\n * to false instead of throwing.\n *\n */\nexport const notify = async (\n\tnotification: Notification,\n\tplatform: string = process.platform,\n): Promise<boolean> => {\n\ttry {\n\t\tconst notifyCommand = buildNotifyCommand(platform, notification);\n\n\t\tif (notifyCommand === null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (notifyCommand.detached) {\n\t\t\treturn release(notifyCommand);\n\t\t}\n\n\t\tawait execFileAsync(notifyCommand.command, notifyCommand.args, {\n\t\t\tkillSignal: \"SIGKILL\",\n\t\t\ttimeout: NOTIFIER_TIMEOUT_MS,\n\t\t});\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n"],"names":["execFile","spawn","fileURLToPath","promisify","execFileAsync","NOTIFIER_TIMEOUT_MS","ALERT_ICON_PATH","URL","url","escapeAppleScript","value","replaceAll","escapePowerShell","macBannerCommand","message","sound","title","script","filter","Boolean","join","args","command","detached","macAlertCommand","dialog","play","linuxCommand","windowsCommand","buildNotifyCommand","platform","notification","banner","release","child","stdio","on","unref","notify","process","notifyCommand","killSignal","timeout"],"mappings":"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"}
package/dist/parse.js CHANGED
@@ -13,6 +13,11 @@ import { defaultFlags } from "./defaults.js";
13
13
  }
14
14
  ],
15
15
  flags: {
16
+ banner: {
17
+ default: defaultFlags.banner,
18
+ description: "Show a passive banner instead of a dialog that waits to be dismissed (macOS only)",
19
+ type: "boolean"
20
+ },
16
21
  boring: {
17
22
  shortFlag: "b",
18
23
  default: defaultFlags.boring,
package/dist/parse.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parse.ts"],"sourcesContent":["/* v8 ignore start */\nimport { parser } from \"@node-cli/parser\";\nimport { defaultFlags } from \"./defaults.js\";\n\nexport type Parameters = {\n\t[\"0\"]?: string;\n};\nexport type Configuration = {\n\tflags?: any;\n\tparameters?: Parameters;\n\tshowHelp?: () => void;\n};\n\n/* v8 ignore next 45 */\nexport const config: Configuration = parser({\n\tmeta: import.meta,\n\texamples: [\n\t\t{\n\t\t\tcommand: \"timer 4h2m15s\",\n\t\t\tcomment: \"## Start a timer for 4 hours, 2 minutes and 15 seconds\",\n\t\t},\n\t\t{\n\t\t\tcommand: \"timer 1m42s\",\n\t\t\tcomment: \"## Start a timer for 1 minute and 42 seconds\",\n\t\t},\n\t],\n\tflags: {\n\t\tboring: {\n\t\t\tshortFlag: \"b\",\n\t\t\tdefault: defaultFlags.boring,\n\t\t\tdescription: \"Do not use color output\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\thelp: {\n\t\t\tshortFlag: \"h\",\n\t\t\tdescription: \"Display help instructions\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\tnotification: {\n\t\t\tshortFlag: \"n\",\n\t\t\tdefault: defaultFlags.notification,\n\t\t\tdescription: \"Display a notification when the timer is done\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\tversion: {\n\t\t\tshortFlag: \"v\",\n\t\t\tdescription: \"Output the current version\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t},\n\tparameters: {\n\t\tformattedTime: {\n\t\t\tdescription: \"Formatted time to start the timer\",\n\t\t},\n\t},\n\n\tusage: \"timer [formattedTime]\",\n});\n/* v8 ignore stop */\n"],"names":["parser","defaultFlags","config","meta","examples","command","comment","flags","boring","shortFlag","default","description","type","help","notification","version","parameters","formattedTime","usage"],"mappings":"AAAA,mBAAmB,GACnB,SAASA,MAAM,QAAQ,mBAAmB;AAC1C,SAASC,YAAY,QAAQ,gBAAgB;AAW7C,qBAAqB,GACrB,OAAO,MAAMC,SAAwBF,OAAO;IAC3CG,MAAM;IACNC,UAAU;QACT;YACCC,SAAS;YACTC,SAAS;QACV;QACA;YACCD,SAAS;YACTC,SAAS;QACV;KACA;IACDC,OAAO;QACNC,QAAQ;YACPC,WAAW;YACXC,SAAST,aAAaO,MAAM;YAC5BG,aAAa;YACbC,MAAM;QACP;QACAC,MAAM;YACLJ,WAAW;YACXE,aAAa;YACbC,MAAM;QACP;QACAE,cAAc;YACbL,WAAW;YACXC,SAAST,aAAaa,YAAY;YAClCH,aAAa;YACbC,MAAM;QACP;QACAG,SAAS;YACRN,WAAW;YACXE,aAAa;YACbC,MAAM;QACP;IACD;IACAI,YAAY;QACXC,eAAe;YACdN,aAAa;QACd;IACD;IAEAO,OAAO;AACR,GAAG,CACH,kBAAkB"}
1
+ {"version":3,"sources":["../src/parse.ts"],"sourcesContent":["/* v8 ignore start */\nimport { parser } from \"@node-cli/parser\";\nimport { defaultFlags } from \"./defaults.js\";\n\nexport type Parameters = {\n\t[\"0\"]?: string;\n};\nexport type Configuration = {\n\tflags?: any;\n\tparameters?: Parameters;\n\tshowHelp?: () => void;\n};\n\n/* v8 ignore next 45 */\nexport const config: Configuration = parser({\n\tmeta: import.meta,\n\texamples: [\n\t\t{\n\t\t\tcommand: \"timer 4h2m15s\",\n\t\t\tcomment: \"## Start a timer for 4 hours, 2 minutes and 15 seconds\",\n\t\t},\n\t\t{\n\t\t\tcommand: \"timer 1m42s\",\n\t\t\tcomment: \"## Start a timer for 1 minute and 42 seconds\",\n\t\t},\n\t],\n\tflags: {\n\t\tbanner: {\n\t\t\tdefault: defaultFlags.banner,\n\t\t\tdescription:\n\t\t\t\t\"Show a passive banner instead of a dialog that waits to be dismissed (macOS only)\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\tboring: {\n\t\t\tshortFlag: \"b\",\n\t\t\tdefault: defaultFlags.boring,\n\t\t\tdescription: \"Do not use color output\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\thelp: {\n\t\t\tshortFlag: \"h\",\n\t\t\tdescription: \"Display help instructions\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\tnotification: {\n\t\t\tshortFlag: \"n\",\n\t\t\tdefault: defaultFlags.notification,\n\t\t\tdescription: \"Display a notification when the timer is done\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t\tversion: {\n\t\t\tshortFlag: \"v\",\n\t\t\tdescription: \"Output the current version\",\n\t\t\ttype: \"boolean\",\n\t\t},\n\t},\n\tparameters: {\n\t\tformattedTime: {\n\t\t\tdescription: \"Formatted time to start the timer\",\n\t\t},\n\t},\n\n\tusage: \"timer [formattedTime]\",\n});\n/* v8 ignore stop */\n"],"names":["parser","defaultFlags","config","meta","examples","command","comment","flags","banner","default","description","type","boring","shortFlag","help","notification","version","parameters","formattedTime","usage"],"mappings":"AAAA,mBAAmB,GACnB,SAASA,MAAM,QAAQ,mBAAmB;AAC1C,SAASC,YAAY,QAAQ,gBAAgB;AAW7C,qBAAqB,GACrB,OAAO,MAAMC,SAAwBF,OAAO;IAC3CG,MAAM;IACNC,UAAU;QACT;YACCC,SAAS;YACTC,SAAS;QACV;QACA;YACCD,SAAS;YACTC,SAAS;QACV;KACA;IACDC,OAAO;QACNC,QAAQ;YACPC,SAASR,aAAaO,MAAM;YAC5BE,aACC;YACDC,MAAM;QACP;QACAC,QAAQ;YACPC,WAAW;YACXJ,SAASR,aAAaW,MAAM;YAC5BF,aAAa;YACbC,MAAM;QACP;QACAG,MAAM;YACLD,WAAW;YACXH,aAAa;YACbC,MAAM;QACP;QACAI,cAAc;YACbF,WAAW;YACXJ,SAASR,aAAac,YAAY;YAClCL,aAAa;YACbC,MAAM;QACP;QACAK,SAAS;YACRH,WAAW;YACXH,aAAa;YACbC,MAAM;QACP;IACD;IACAM,YAAY;QACXC,eAAe;YACdR,aAAa;QACd;IACD;IAEAS,OAAO;AACR,GAAG,CACH,kBAAkB"}
package/dist/timer.js CHANGED
File without changes
package/dist/utilities.js CHANGED
@@ -81,6 +81,7 @@ export const extractDuration = (config)=>{
81
81
  */ if (this.config.flags.notification) {
82
82
  try {
83
83
  await notify({
84
+ banner: this.config.flags.banner,
84
85
  message,
85
86
  sound: "Funk",
86
87
  title: "Timer Notification"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utilities.ts"],"sourcesContent":["import { Logger, Spinner } from \"@node-cli/logger\";\nimport moment, { Duration } from \"moment\";\n\nimport { notify } from \"./notify.js\";\nimport { Configuration } from \"./parse.js\";\n\nconst logger = new Logger();\n\n/**\n * discardStdin must stay off. When it is on, ora puts stdin in raw mode, which\n * stops the tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte\n * instead. stdin-discarder means to re-send the signal itself, but it attaches\n * its reader with prependListener (which, unlike on(\"data\"), does not start a\n * paused stream flowing) and only calls resume() when stdin.isPaused() was true\n * beforehand. On a fresh process.stdin, flowing is null, so isPaused() is false\n * and the resume never happens: the byte is never read, the signal is never\n * re-sent, and Ctrl+C is swallowed for the life of the spinner.\n */\nconst spinner = new Spinner({ discardStdin: false });\n\nconst UNITS = {\n\tms: 1000,\n\ts: 1000 * 1000,\n\tm: 1000 * 1000 * 60,\n\th: 1000 * 1000 * 60 * 60,\n\td: 1000 * 1000 * 60 * 60 * 24,\n\tw: 1000 * 1000 * 60 * 60 * 24 * 7,\n};\n\nexport const timeToMicroseconds = (value: string, unit: string) => {\n\tconst result = UNITS[unit];\n\treturn result ? Number.parseInt(value, 10) * result : 0;\n};\n\nexport const extractDuration = (config: Configuration): Duration => {\n\tconst { parameters, showHelp } = config;\n\tlet totalMicroseconds = 0;\n\n\t/* v8 ignore next 6 */\n\tif (\n\t\tObject.entries(parameters).length === 0 ||\n\t\tObject.entries(parameters).length > 1\n\t) {\n\t\tshowHelp();\n\t}\n\n\tconst groups = parameters[\"0\"].toLowerCase().match(/[+-]?[\\d.]+[a-z]+/g);\n\n\t/* v8 ignore next 2 */\n\tif (groups === null) {\n\t\tshowHelp();\n\t} else {\n\t\tfor (const g of groups) {\n\t\t\tconst value = g.match(/[\\d.]+/g)[0];\n\t\t\tconst unit = g.match(/[a-z]+/g)[0];\n\t\t\ttotalMicroseconds += timeToMicroseconds(value, unit);\n\t\t}\n\t\treturn moment.duration(totalMicroseconds / UNITS.ms);\n\t}\n};\n\n/* v8 ignore start */\nexport class Timer {\n\tconfig: Configuration;\n\tstartTime: number;\n\ttimerDurationMilliSeconds: number;\n\tintroMessage: string;\n\ttimerDurationSeconds: number;\n\n\tconstructor(config: Configuration) {\n\t\tthis.config = config;\n\t\tconst result: Duration = extractDuration(this.config);\n\t\tthis.timerDurationMilliSeconds = result.asMilliseconds();\n\t\tthis.timerDurationSeconds = result.asSeconds();\n\t\tthis.introMessage = `${result.hours()}h ${result.minutes()}m ${result.seconds()}s`;\n\t}\n\n\tstart = () => {\n\t\tif (this.timerDurationMilliSeconds > 0) {\n\t\t\tlogger.printBox(`${this.introMessage} `, {\n\t\t\t\tpadding: 0,\n\t\t\t\tborderColor: \"cyan\",\n\t\t\t\ttitle: \"Timer Configuration\",\n\t\t\t});\n\n\t\t\tthis.startTime = Date.now();\n\t\t\tspinner.start(this.introMessage);\n\n\t\t\t// update the spinner every second.\n\t\t\tthis.printRemainingTime();\n\t\t\tconst timer = setInterval(this.printRemainingTime, 1000);\n\n\t\t\t// kill the spinner when the timer is done.\n\t\t\tsetTimeout(async () => {\n\t\t\t\tconst message = \"Time's up!\";\n\t\t\t\tclearInterval(timer);\n\t\t\t\tthis.printRemainingTime(message);\n\t\t\t\tspinner.stop();\n\n\t\t\t\t/**\n\t\t\t\t * notifying last, and never fatally: a desktop notification that cannot be\n\t\t\t\t * displayed must not take the timer down with it.\n\t\t\t\t */\n\t\t\t\tif (this.config.flags.notification) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait notify({\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tsound: \"Funk\",\n\t\t\t\t\t\t\ttitle: \"Timer Notification\",\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * notify is already total, so this only ever catches a future regression\n\t\t\t\t\t\t * in it. Rejecting here would be fatal: an async setTimeout callback has\n\t\t\t\t\t\t * nothing to attach a handler to.\n\t\t\t\t\t\t */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, this.timerDurationMilliSeconds);\n\t\t}\n\t};\n\n\tprivate printRemainingTime = (message: string | void) => {\n\t\tif (message) {\n\t\t\tspinner.text = message;\n\t\t} else {\n\t\t\tconst elapsed = (Date.now() - this.startTime) / 1000;\n\t\t\tconst duration = moment.duration(\n\t\t\t\t(this.timerDurationSeconds - elapsed) * 1000,\n\t\t\t);\n\t\t\tspinner.text = `${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;\n\t\t}\n\t};\n}\n/* v8 ignore stop */\n"],"names":["Logger","Spinner","moment","notify","logger","spinner","discardStdin","UNITS","ms","s","m","h","d","w","timeToMicroseconds","value","unit","result","Number","parseInt","extractDuration","config","parameters","showHelp","totalMicroseconds","Object","entries","length","groups","toLowerCase","match","g","duration","Timer","startTime","timerDurationMilliSeconds","introMessage","timerDurationSeconds","asMilliseconds","asSeconds","hours","minutes","seconds","start","printBox","padding","borderColor","title","Date","now","printRemainingTime","timer","setInterval","setTimeout","message","clearInterval","stop","flags","notification","sound","text","elapsed"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/utilities.ts"],"sourcesContent":["import { Logger, Spinner } from \"@node-cli/logger\";\nimport moment, { Duration } from \"moment\";\n\nimport { notify } from \"./notify.js\";\nimport { Configuration } from \"./parse.js\";\n\nconst logger = new Logger();\n\n/**\n * discardStdin must stay off. When it is on, ora puts stdin in raw mode, which\n * stops the tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte\n * instead. stdin-discarder means to re-send the signal itself, but it attaches\n * its reader with prependListener (which, unlike on(\"data\"), does not start a\n * paused stream flowing) and only calls resume() when stdin.isPaused() was true\n * beforehand. On a fresh process.stdin, flowing is null, so isPaused() is false\n * and the resume never happens: the byte is never read, the signal is never\n * re-sent, and Ctrl+C is swallowed for the life of the spinner.\n */\nconst spinner = new Spinner({ discardStdin: false });\n\nconst UNITS = {\n\tms: 1000,\n\ts: 1000 * 1000,\n\tm: 1000 * 1000 * 60,\n\th: 1000 * 1000 * 60 * 60,\n\td: 1000 * 1000 * 60 * 60 * 24,\n\tw: 1000 * 1000 * 60 * 60 * 24 * 7,\n};\n\nexport const timeToMicroseconds = (value: string, unit: string) => {\n\tconst result = UNITS[unit];\n\treturn result ? Number.parseInt(value, 10) * result : 0;\n};\n\nexport const extractDuration = (config: Configuration): Duration => {\n\tconst { parameters, showHelp } = config;\n\tlet totalMicroseconds = 0;\n\n\t/* v8 ignore next 6 */\n\tif (\n\t\tObject.entries(parameters).length === 0 ||\n\t\tObject.entries(parameters).length > 1\n\t) {\n\t\tshowHelp();\n\t}\n\n\tconst groups = parameters[\"0\"].toLowerCase().match(/[+-]?[\\d.]+[a-z]+/g);\n\n\t/* v8 ignore next 2 */\n\tif (groups === null) {\n\t\tshowHelp();\n\t} else {\n\t\tfor (const g of groups) {\n\t\t\tconst value = g.match(/[\\d.]+/g)[0];\n\t\t\tconst unit = g.match(/[a-z]+/g)[0];\n\t\t\ttotalMicroseconds += timeToMicroseconds(value, unit);\n\t\t}\n\t\treturn moment.duration(totalMicroseconds / UNITS.ms);\n\t}\n};\n\n/* v8 ignore start */\nexport class Timer {\n\tconfig: Configuration;\n\tstartTime: number;\n\ttimerDurationMilliSeconds: number;\n\tintroMessage: string;\n\ttimerDurationSeconds: number;\n\n\tconstructor(config: Configuration) {\n\t\tthis.config = config;\n\t\tconst result: Duration = extractDuration(this.config);\n\t\tthis.timerDurationMilliSeconds = result.asMilliseconds();\n\t\tthis.timerDurationSeconds = result.asSeconds();\n\t\tthis.introMessage = `${result.hours()}h ${result.minutes()}m ${result.seconds()}s`;\n\t}\n\n\tstart = () => {\n\t\tif (this.timerDurationMilliSeconds > 0) {\n\t\t\tlogger.printBox(`${this.introMessage} `, {\n\t\t\t\tpadding: 0,\n\t\t\t\tborderColor: \"cyan\",\n\t\t\t\ttitle: \"Timer Configuration\",\n\t\t\t});\n\n\t\t\tthis.startTime = Date.now();\n\t\t\tspinner.start(this.introMessage);\n\n\t\t\t// update the spinner every second.\n\t\t\tthis.printRemainingTime();\n\t\t\tconst timer = setInterval(this.printRemainingTime, 1000);\n\n\t\t\t// kill the spinner when the timer is done.\n\t\t\tsetTimeout(async () => {\n\t\t\t\tconst message = \"Time's up!\";\n\t\t\t\tclearInterval(timer);\n\t\t\t\tthis.printRemainingTime(message);\n\t\t\t\tspinner.stop();\n\n\t\t\t\t/**\n\t\t\t\t * notifying last, and never fatally: a desktop notification that cannot be\n\t\t\t\t * displayed must not take the timer down with it.\n\t\t\t\t */\n\t\t\t\tif (this.config.flags.notification) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait notify({\n\t\t\t\t\t\t\tbanner: this.config.flags.banner,\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tsound: \"Funk\",\n\t\t\t\t\t\t\ttitle: \"Timer Notification\",\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * notify is already total, so this only ever catches a future regression\n\t\t\t\t\t\t * in it. Rejecting here would be fatal: an async setTimeout callback has\n\t\t\t\t\t\t * nothing to attach a handler to.\n\t\t\t\t\t\t */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, this.timerDurationMilliSeconds);\n\t\t}\n\t};\n\n\tprivate printRemainingTime = (message: string | void) => {\n\t\tif (message) {\n\t\t\tspinner.text = message;\n\t\t} else {\n\t\t\tconst elapsed = (Date.now() - this.startTime) / 1000;\n\t\t\tconst duration = moment.duration(\n\t\t\t\t(this.timerDurationSeconds - elapsed) * 1000,\n\t\t\t);\n\t\t\tspinner.text = `${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;\n\t\t}\n\t};\n}\n/* v8 ignore stop */\n"],"names":["Logger","Spinner","moment","notify","logger","spinner","discardStdin","UNITS","ms","s","m","h","d","w","timeToMicroseconds","value","unit","result","Number","parseInt","extractDuration","config","parameters","showHelp","totalMicroseconds","Object","entries","length","groups","toLowerCase","match","g","duration","Timer","startTime","timerDurationMilliSeconds","introMessage","timerDurationSeconds","asMilliseconds","asSeconds","hours","minutes","seconds","start","printBox","padding","borderColor","title","Date","now","printRemainingTime","timer","setInterval","setTimeout","message","clearInterval","stop","flags","notification","banner","sound","text","elapsed"],"mappings":"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"}
package/package.json CHANGED
@@ -1,50 +1,52 @@
1
1
  {
2
- "name": "@node-cli/timer",
3
- "version": "2.1.2",
4
- "license": "MIT",
5
- "author": "Arno Versini",
6
- "description": "Dead simpler CLI timer",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/versini-org/node-cli.git",
10
- "directory": "packages/timer"
11
- },
12
- "type": "module",
13
- "types": "./dist/index.d.ts",
14
- "exports": "./dist/timer.js",
15
- "bin": "dist/timer.js",
16
- "files": [
17
- "dist",
18
- "README.md"
19
- ],
20
- "engines": {
21
- "node": ">=22"
22
- },
23
- "scripts": {
24
- "build": "npm-run-all --serial clean build:types build:js build:barrel",
25
- "build:barrel": "barrelsby --delete --directory dist --pattern \"**/*.d.ts\" --name \"index.d\"",
26
- "build:js": "swc --strip-leading-paths --source-maps --out-dir dist src",
27
- "build:types": "tsc",
28
- "clean": "rimraf dist types coverage",
29
- "comments:fix": "comments --merge-line-comments 'src/**/*.ts'",
30
- "lint": "biome lint src",
31
- "lint:fix": "biome check src --write --no-errors-on-unmatched",
32
- "test": "vitest run --globals",
33
- "test:coverage": "vitest run --coverage --globals",
34
- "test:watch": "vitest --globals",
35
- "watch": "swc --strip-leading-paths --watch --out-dir dist src"
36
- },
37
- "dependencies": {
38
- "@node-cli/logger": "2.1.0",
39
- "@node-cli/parser": "3.1.0",
40
- "moment": "2.30.1"
41
- },
42
- "publishConfig": {
43
- "access": "public"
44
- },
45
- "devDependencies": {
46
- "@vitest/coverage-v8": "4.1.10",
47
- "vitest": "4.1.10"
48
- },
49
- "gitHead": "9fdd995083ea81292321f20a3397122a18369b87"
50
- }
2
+ "name": "@node-cli/timer",
3
+ "version": "2.2.1",
4
+ "license": "MIT",
5
+ "author": "Arno Versini",
6
+ "description": "Dead simpler CLI timer",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/versini-org/node-cli.git",
10
+ "directory": "packages/timer"
11
+ },
12
+ "type": "module",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": "./dist/timer.js",
15
+ "files": [
16
+ "assets",
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "dependencies": {
24
+ "moment": "2.30.1",
25
+ "@node-cli/logger": "2.1.1",
26
+ "@node-cli/parser": "3.1.1"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "devDependencies": {
32
+ "@vitest/coverage-v8": "4.1.11",
33
+ "vitest": "4.1.11"
34
+ },
35
+ "scripts": {
36
+ "build": "npm-run-all --serial clean build:types build:js build:barrel",
37
+ "build:barrel": "barrelsby --delete --directory dist --pattern \"**/*.d.ts\" --name \"index.d\"",
38
+ "build:js": "swc --strip-leading-paths --source-maps --out-dir dist src",
39
+ "build:types": "tsc",
40
+ "clean": "rimraf dist types coverage",
41
+ "comments:fix": "comments --merge-line-comments 'src/**/*.ts'",
42
+ "lint": "biome lint src",
43
+ "lint:fix": "biome check src --write --no-errors-on-unmatched",
44
+ "test": "vitest run --globals",
45
+ "test:coverage": "vitest run --coverage --globals",
46
+ "test:watch": "vitest --globals",
47
+ "watch": "swc --strip-leading-paths --watch --out-dir dist src"
48
+ },
49
+ "bin": {
50
+ "timer": "dist/timer.js"
51
+ }
52
+ }