smart-spinner 1.0.1 → 1.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/.ocmux.json ADDED
@@ -0,0 +1 @@
1
+ {"url":"http://127.0.0.1:33469","logfile":"/tmp/opencode-serve-3370b8294dc3.log","window_index":4}
package/AGENTS.md ADDED
@@ -0,0 +1,19 @@
1
+ # smart-spinner — agent guide
2
+
3
+ Single-file npm package (`index.js`). No build, no tests, no CI.
4
+
5
+ ## Commands
6
+
7
+ - **Run examples:** `node examples/simple-example.js` (from project root; uses `require("../")`)
8
+ - **Test:** none — `npm test` is a no-op stub
9
+
10
+ ## Key architecture
11
+
12
+ - `spinner.create(msg, opts)` → returns `ctrlAPI(stopOrMsg, newMsg?)` — `opts` is an options object (`{ spinners, timestamp }`). For backward compat, a string/array is accepted as shorthand for `{ spinners: value }`. The returned API is overloaded: pass a `boolean` to stop, a `string`/`function` to update the message, or both. Also has `ctrlAPI.update(newMsg)` for non-TTY phase transitions.
13
+ - `opts.timestamp` (`'utc'`, `'local'`, or IANA timezone) prepends ISO-8601 timestamps to non-TTY log lines.
14
+ - Non-TTY detection (`tty.isatty`) silences animation; only the initial message + updates are `console.log`-ed once.
15
+ - `has-unicode` / `chalk` graceful fallback for non-Unicode / no-color terminals.
16
+
17
+ ## Dependencies (old majors)
18
+
19
+ `chalk@^2`, `cli-spinners@^1`, `log-update@^2`, `has-unicode@^2` — be aware if bumping.
package/README.md CHANGED
@@ -84,7 +84,7 @@ const spinner = require("smart-spinner");
84
84
 
85
85
 
86
86
  ```javascript
87
- var progress = spinner.create(<message> [, <spinner_list>]);
87
+ var progress = spinner.create(<message> [, <opts>]);
88
88
  ```
89
89
 
90
90
  **Where:**
@@ -94,25 +94,38 @@ var progress = spinner.create(<message> [, <spinner_list>]);
94
94
  is actually connected to terminal or not. So complex calculations can be
95
95
  avoided if needed (also callback will be called single time in this case).
96
96
 
97
- * `<spinner_list>`: Array of spinner names (See [list()](#list) or
98
- [demo()](#demo) to know which ones are available) or string "all" to use
99
- all.
100
- - Specified spinners will be rotated regularly.
101
- - If "all" keyword is specified instead, all available ones will be used.
102
- - If "demo" keyword is specified instead, it will operate just like if
103
- [demo()](#demo) were used instead.
104
- - If string (distinct of "all" and "demo" is used) it wil be threated as
105
- single spinner list.
97
+ * `<opts>`: Optional options object with the following properties:
106
98
 
99
+ - `spinners` (string|Array): Spinner name or list of spinner names (See
100
+ [list()](#list) or [demo()](#demo) to know which ones are available).
101
+ Use `"all"` to rotate through all available spinners, or `"demo"` to
102
+ show spinner names as they rotate. Default: `['dots']`.
107
103
 
108
- **Return value:** Callback to control spinner:
104
+ - `timestamp` (boolean|string): Prepend ISO-8601 timestamp to non-TTY
105
+ log lines. Use `'utc'` (or `true`) for UTC, `'local'` for local
106
+ timezone, or any IANA timezone string (e.g. `'America/New_York'`).
107
+ Default: none.
109
108
 
110
- __Syntax:__
109
+ > **Note:** For backward compatibility, the second parameter also accepts a
110
+ > string or array directly as a shorthand for `{ spinners: <value> }`.
111
+ > The old 3-argument form `create(msg, spinners, opts)` is deprecated.
112
+
113
+
114
+ **Return value:** Control API with the following methods:
115
+
116
+ __Function call syntax:__
111
117
 
112
118
  * `progress (String <newMsg>)`: Set message to provided string or callback.
113
119
  * `progress (Boolean <stop>)`: Stop spinning with success (stop = true) or failed (stop = false).
114
120
  * `progress (Boolean <stop>, String <newMsg>)` Stop spinning but also updates message.
115
121
 
122
+ __Method syntax:__
123
+
124
+ * `progress.update(String|Function <newMsg>)`: Replace the message callback
125
+ and immediately emit a new line in non-TTY mode (does nothing on
126
+ interactive terminals — the animation loop picks up the new callback
127
+ automatically).
128
+
116
129
 
117
130
 
118
131
  ### Other functions
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // Run me with 'node path/to/examples/simple-example.js'.
3
+
4
+ const spinner = require("../");
5
+
6
+ var progress = spinner.create("Here a Spinner with timestamp", {timestamp: "local"});
7
+
8
+ // Also try using demo() instead:
9
+ // var progress = spinner.demo();
10
+
11
+ setTimeout(()=>{
12
+ progress("If you don't see it, try to pipe the output to a file or a command like `cat`.");
13
+ }, 2000);
14
+
15
+
16
+ setTimeout(()=>{
17
+ progress(function(){
18
+ var someStatus = (new Date()).toLocaleString();
19
+ return "You already know that current time is: " + someStatus;
20
+ });
21
+ }, 4000);
22
+
23
+
24
+ setTimeout(()=>{
25
+ progress("But if you read a logged file later, you won't.");
26
+ }, 6000);
27
+
28
+ setTimeout(()=>Math.round(Math.random())
29
+ ? progress(true, "Job done!!")
30
+ : progress(false, "Error!!")
31
+ , 8000);
package/index.js CHANGED
@@ -39,7 +39,14 @@ if (
39
39
  // ===================//}}}
40
40
 
41
41
 
42
- function smartSpinner(cbk, spinners = ['dots']) {//{{{
42
+ function smartSpinner(cbk, opts = {}) {//{{{
43
+
44
+ // Backward compat: if opts is a string or array, treat as spinner list:
45
+ if (typeof opts === 'string' || Array.isArray(opts)) {
46
+ opts = { spinners: opts };
47
+ };
48
+
49
+ var spinners = opts.spinners !== undefined ? opts.spinners : ['dots'];
43
50
 
44
51
  var messageCbk;
45
52
 
@@ -49,6 +56,45 @@ function smartSpinner(cbk, spinners = ['dots']) {//{{{
49
56
  let next;
50
57
  let is_tty = tty.isatty(process.stdout.fd);
51
58
 
59
+ var tsEnabled = !!(opts && opts.timestamp);
60
+ var tsOpt = opts && opts.timestamp;
61
+ if (tsEnabled) {
62
+ if (tsOpt === true) tsOpt = 'utc';
63
+ if (typeof tsOpt !== 'string') tsOpt = 'utc';
64
+ };
65
+ function fmtTS() {
66
+ if (!tsEnabled) return '';
67
+ var now = new Date();
68
+ var s;
69
+ if (tsOpt === 'utc') {
70
+ s = now.toISOString().replace(/\.\d+Z$/, 'Z');
71
+ } else {
72
+ var dtfOpt = {
73
+ year: 'numeric', month: '2-digit', day: '2-digit',
74
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
75
+ hour12: false, timeZoneName: 'shortOffset'
76
+ };
77
+ if (tsOpt !== 'local') dtfOpt.timeZone = tsOpt;
78
+ var map = {};
79
+ Intl.DateTimeFormat('en-CA', dtfOpt).formatToParts(now).forEach(function(p) { map[p.type] = p.value; });
80
+ var tz = map.timeZoneName || 'Z';
81
+ if (tz === 'UTC') tz = 'Z';
82
+ else if (tz.slice(0, 3) === 'GMT') {
83
+ var off = tz.slice(3);
84
+ if (!off) { tz = 'Z'; }
85
+ else {
86
+ var sign = off.charAt(0);
87
+ var rest = off.slice(1);
88
+ var parts = rest.split(':');
89
+ var hh = parts[0].length < 2 ? '0' + parts[0] : parts[0];
90
+ var mm = parts[1] ? (parts[1].length < 2 ? '0' + parts[1] : parts[1]) : '00';
91
+ tz = sign + hh + ':' + mm;
92
+ };
93
+ };
94
+ s = map.year + '-' + map.month + '-' + map.day + 'T' + map.hour + ':' + map.minute + ':' + map.second + tz;
95
+ };
96
+ return '[' + s + '] ';
97
+ };
52
98
 
53
99
  function showNextFrame () {//{{{
54
100
  const frames = cliSpinners[spinners[spinner]].frames;
@@ -77,7 +123,7 @@ function smartSpinner(cbk, spinners = ['dots']) {//{{{
77
123
  if (stop !== undefined) {
78
124
  stop || process.stdout.write("\n");
79
125
  } else {
80
- console.log(symbols.arrow + ' ' + messageCbk(false));
126
+ console.log(fmtTS() + symbols.arrow + ' ' + messageCbk(false));
81
127
  };
82
128
  };
83
129
  };
@@ -88,15 +134,25 @@ function smartSpinner(cbk, spinners = ['dots']) {//{{{
88
134
  stopped = true;
89
135
 
90
136
  let s = symbols[stop ? "ok" : "error"];
91
- logUpdate(s + ' ' + messageCbk(true));
92
-
93
- if (is_tty) process.stdout.write("\n");
137
+ if (is_tty) {
138
+ logUpdate(s + ' ' + messageCbk(true));
139
+ process.stdout.write("\n");
140
+ } else {
141
+ console.log(fmtTS() + s + ' ' + messageCbk(true));
142
+ };
94
143
  }
95
144
 
96
145
  return true; // Success.
97
146
 
98
147
  };//}}}
99
148
 
149
+ ctrlAPI.update = function (newCbk) {
150
+ messageCbk = ("function" == typeof newCbk) ? newCbk : ()=>newCbk;
151
+ if (!is_tty) {
152
+ console.log(fmtTS() + symbols.arrow + ' ' + messageCbk(false));
153
+ };
154
+ };
155
+
100
156
  ctrlAPI(cbk);
101
157
 
102
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-spinner",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Simple cli-spinners wrapper.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,191 @@
1
+ # smart-spinner: pending modifications
2
+
3
+ ## Task 1: Remove `\r` control characters from non-TTY output on stop
4
+
5
+ **Location:** `index.js`, stop block (lines ~86–94)
6
+
7
+ **Problem:** When output is not a TTY (e.g. `cmt_import | tee log`), the stop
8
+ path calls `logUpdate()` unconditionally. `logUpdate` writes `\r` to overwrite
9
+ the current line, which is fine on a terminal but leaves literal `\r` garbage
10
+ in log files when viewed in editors like vim (though `cat`/`less` handle them).
11
+
12
+ **Fix:** In the stop block (`if (stop !== undefined) { ... }`), branch on
13
+ `is_tty`:
14
+
15
+ ```js
16
+ if (is_tty) {
17
+ logUpdate(s + ' ' + messageCbk(true));
18
+ process.stdout.write('\n');
19
+ } else {
20
+ console.log(s + ' ' + messageCbk(true));
21
+ }
22
+ ```
23
+
24
+ This mirrors the existing non-TTY branch already used in the update path
25
+ (around line 80): `console.log` writes plain text with a final newline, no
26
+ `\r`.
27
+
28
+ ## Task 2: Expose an `update()` method so the client can trigger a new non-TTY log line
29
+
30
+ **Location:** `index.js`, the control API returned by `spinner.create()`
31
+
32
+ **Problem:** Smart-spinner's `create(fn)` takes a single callback. When the
33
+ client's internal phase changes (e.g. from md5-computation to XML-loading),
34
+ the client needs the spinner to emit a fresh progress line in non-TTY mode.
35
+ Currently, the non-TTY `console.log` only fires when the callback is first
36
+ set (line ~80), so phase transitions stay silent — the log never shows the
37
+ new phase message.
38
+
39
+ The client workaround is to stop and re-create the spinner, which is
40
+ cumbersome and loses the visual continuity.
41
+
42
+ **Fix:** Add an `update()` method to the control API that:
43
+ 1. Replaces the internal callback reference.
44
+ 2. If `!is_tty`, immediately calls `console.log(messageCbk(false))` to emit
45
+ the new message.
46
+
47
+ Signature: `ctrlAPI.update(newCallback)` — `newCallback` replaces the
48
+ original callback and a non-TTY line is written synchronously.
49
+
50
+ If `is_tty` (interactive terminal), `update()` does nothing — the animation
51
+ loop already picks up the new callback on the next tick, so no special
52
+ handling is needed.
53
+
54
+ ```js
55
+ ctrlAPI.update = function (newCbk) {
56
+ messageCbk = newCbk;
57
+ if (!is_tty) {
58
+ console.log(messageCbk(false));
59
+ }
60
+ };
61
+ ```
62
+
63
+ **Client usage:**
64
+
65
+ ```js
66
+ var progress = spinner.create(function (interactive) {
67
+ if (phase === 'md5') return interactive ? 'Hashing...' : 'Hashing...';
68
+ return interactive ? 'Loading...' : 'Loading...';
69
+ });
70
+
71
+ // When phase changes:
72
+ phase = 'loading';
73
+ progress.update(function (interactive) {
74
+ return 'Loading...';
75
+ });
76
+ ```
77
+
78
+ This is purely additive — no existing behaviour changes. The client is free
79
+ to ignore `update()` and just stop/recreate as before.
80
+
81
+ ## Task 3: Add timestamp prefix to non-TTY output
82
+
83
+ **Location:** `index.js`, the `spinner.create()` function signature and all
84
+ `console.log` call sites in the non-TTY branches.
85
+
86
+ **Problem:** When reading log files from a non-interactive run, there's no way
87
+ to tell when each progress line was emitted. The client currently works around
88
+ this by embedding timestamps in the callback message itself, but this is
89
+ verbose, inconsistent, and forces every client to reimplement the same logic.
90
+
91
+ **Goal:** Add an optional `timestamp` parameter to `spinner.create` that, when
92
+ enabled, prepends an ISO-8601 timestamp to every non-TTY output line.
93
+
94
+ **API change:**
95
+
96
+ ```js
97
+ // Current — no timestamps (backward compatible):
98
+ spinner.create(cbk)
99
+ spinner.create(cbk, {})
100
+
101
+ // New — with timestamps:
102
+ spinner.create(cbk, { timestamp: 'utc' }) // [2026-06-05T13:03:40Z]
103
+ spinner.create(cbk, { timestamp: 'local' }) // [2026-06-05T15:03:40+02:00]
104
+ ```
105
+
106
+ - When `opts.timestamp` is `true`, behave as `'utc'`.
107
+ - When falsy or absent, no timestamp is prepended.
108
+ - Any other string value is passed through to the `Intl.DateTimeFormat`
109
+ `timeZone` option (e.g. `'America/New_York'`), so it Just Works for any
110
+ IANA timezone.
111
+
112
+ **Implementation:**
113
+
114
+ 1. Change the `create` signature from `create(messageCbk)` to
115
+ `create(messageCbk, opts)` where `opts` defaults to `{}`.
116
+
117
+ 2. Compute a `ts` helper function (or inline it) at the top of the function
118
+ body:
119
+
120
+ ```js
121
+ var tsEnabled = !!(opts && opts.timestamp);
122
+ var tsOpt = opts && opts.timestamp;
123
+ if (tsEnabled) {
124
+ // true → 'utc', already a string → use as-is, else 'utc'
125
+ if (tsOpt === true) tsOpt = 'utc';
126
+ if (typeof tsOpt !== 'string') tsOpt = 'utc';
127
+ }
128
+ function fmtTS() {
129
+ if (!tsEnabled) return '';
130
+ var now = new Date();
131
+ var s;
132
+ if (tsOpt === 'utc') {
133
+ s = now.toISOString(); // always Z
134
+ } else {
135
+ // local or arbitrary IANA timezone
136
+ var opt = { year: 'numeric', month: '2-digit', day: '2-digit',
137
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
138
+ timeZoneName: 'shortOffset', timeZone: tsOpt };
139
+ var parts = Intl.DateTimeFormat('en-CA', opt).formatToParts(now);
140
+ // format as YYYY-MM-DDTHH:mm:ss±HH:mm (ISO 8601)
141
+ // … assembly logic
142
+ }
143
+ return '[' + s + '] ';
144
+ }
145
+ ```
146
+
147
+ 3. Prepend `fmtTS()` to every non-TTY `console.log` call. There are three
148
+ such sites:
149
+ - The initial log line when the callback is first registered (around line 80):
150
+ `console.log(fmtTS() + '→ ' + messageCbk(false));`
151
+ - The `update()` path added in Task 2:
152
+ `console.log(fmtTS() + messageCbk(false));`
153
+ - The stop path (Task 1):
154
+ `console.log(fmtTS() + s + ' ' + messageCbk(true));`
155
+
156
+ (The `→` symbol is only used in the initial/update log lines, not in the
157
+ stop line, since the stop already has the ✓/✗ symbol.)
158
+
159
+ 4. The interactive (TTY) path is **not** modified — timestamps would clutter
160
+ the animated spinner line.
161
+
162
+ **Client migration:**
163
+
164
+ Once this is published, the client removes its own timestamp formatting and
165
+ passes the option instead:
166
+
167
+ ```js
168
+ // Before (cmt_import.js):
169
+ var progress = spinner.create(function (interactive) {
170
+ if (phase === 'md5') return interactive
171
+ ? "Calculating file hash (md5)..."
172
+ : "[" + new Date().toISOString() + "] Calculating file hash (md5)...";
173
+ // ...
174
+ });
175
+
176
+ // After:
177
+ var progress = spinner.create(function (interactive) {
178
+ if (phase === 'md5') return "Calculating file hash (md5)...";
179
+ // ...
180
+ }, { timestamp: 'utc' });
181
+ ```
182
+
183
+ The `update()` method receives the same `opts` automatically (no need to
184
+ pass it again on every call).
185
+
186
+ **Edge cases:**
187
+ - `Intl.DateTimeFormat` throws on invalid timezone strings. Let it propagate
188
+ — the developer will fix their config.
189
+ - `en-CA` locale gives `YYYY-MM-DD` format naturally. If the browser/runtime
190
+ doesn't support `en-CA`, fall back to manual `padStart` construction.
191
+ (Node.js supports it since v13, so this is not a concern in practice.)