[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

fix(date): resolve timezone issues in DatePrompt (#486)

Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com>

authored by

HarshaVardhan
paul valladares
and committed by
GitHub
(Mar 14, 2026, 1:19 PM -0500) 52fce8a6 090902cf

+81 -25
+6
.changeset/early-ads-tap.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + "@clack/core": patch 4 + --- 5 + 6 + Fix timezone issues in DatePrompt causing dates to be off by one day in non-UTC timezones
+24 -10
packages/core/src/prompts/date.ts
··· 31 31 return { year: '____', month: '__', day: '__' }; 32 32 } 33 33 return { 34 - year: String(date.getFullYear()).padStart(4, '0'), 35 - month: String(date.getMonth() + 1).padStart(2, '0'), 36 - day: String(date.getDate()).padStart(2, '0'), 34 + year: String(date.getUTCFullYear()).padStart(4, '0'), 35 + month: String(date.getUTCMonth() + 1).padStart(2, '0'), 36 + day: String(date.getUTCDate()).padStart(2, '0'), 37 37 }; 38 38 } 39 39 40 - function segmentValuesToParsed(parts: DateParts): { year: number; month: number; day: number } { 40 + function segmentValuesToParsed(parts: DateParts): { 41 + year: number; 42 + month: number; 43 + day: number; 44 + } { 41 45 const val = (s: string) => Number.parseInt((s || '0').replace(/_/g, '0'), 10) || 0; 42 46 return { 43 47 year: val(parts.year), ··· 119 123 if (!year || year < 1000 || year > 9999) return undefined; 120 124 if (!month || month < 1 || month > 12) return undefined; 121 125 if (!day || day < 1) return undefined; 122 - const date = new Date(year, month - 1, day); 123 - if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) { 126 + const date = new Date(Date.UTC(year, month - 1, day)); 127 + if ( 128 + date.getUTCFullYear() !== year || 129 + date.getUTCMonth() !== month - 1 || 130 + date.getUTCDate() !== day 131 + ) { 124 132 return undefined; 125 133 } 126 134 return { year, month, day }; 127 135 } 128 136 129 - /** Build a Date from segment values using local midnight so getFullYear/getMonth/getDate are timezone-stable. */ 137 + /** Build a Date from segment values using UTC midnight so getFullYear/getMonth/getDate are timezone-stable. */ 130 138 function segmentValuesToDate(parts: DateParts): Date | undefined { 131 139 const parsed = segmentValuesToParts(parts); 132 140 if (!parsed) return undefined; 133 - return new Date(parsed.year, parsed.month - 1, parsed.day); 141 + return new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day)); 134 142 } 135 143 136 144 function segmentValuesToISOString(parts: DateParts): string | undefined { ··· 278 286 : clamp(bounds.min, num + direction, bounds.max); 279 287 280 288 const newSegmentValue = String(newNum).padStart(segment.len, '0'); 281 - this.#segmentValues = { ...this.#segmentValues, [segment.type]: newSegmentValue }; 289 + this.#segmentValues = { 290 + ...this.#segmentValues, 291 + [segment.type]: newSegmentValue, 292 + }; 282 293 this.#refreshFromSegmentValues(); 283 294 } 284 295 ··· 331 342 const newSegmentVal = segmentDisplay.slice(0, pos) + char + segmentDisplay.slice(pos + 1); 332 343 333 344 if (!newSegmentVal.includes('_')) { 334 - const newParts = { ...this.#segmentValues, [segment.type]: newSegmentVal }; 345 + const newParts = { 346 + ...this.#segmentValues, 347 + [segment.type]: newSegmentVal, 348 + }; 335 349 const validationMsg = getSegmentValidationMessage(newParts, segment); 336 350 if (validationMsg) { 337 351 this.inlineError = validationMsg;
+50 -14
packages/core/test/prompts/date.test.ts
··· 32 32 33 33 const d = (iso: string) => { 34 34 const [y, m, day] = iso.slice(0, 10).split('-').map(Number); 35 - return new Date(y, m - 1, day); 35 + return new Date(Date.UTC(y, m - 1, day)); 36 36 }; 37 37 38 38 describe('DatePrompt', () => { ··· 70 70 instance.prompt(); 71 71 expect(instance.userInput).to.equal('2025/01/15'); 72 72 expect(instance.value).toBeInstanceOf(Date); 73 - expect(instance.value!.toISOString().slice(0, 10)).to.equal('2025-01-15'); 73 + expect(instance.value?.toISOString().slice(0, 10)).to.equal('2025-01-15'); 74 74 }); 75 75 76 76 test('left/right navigates between segments', () => { ··· 82 82 initialValue: d('2025-01-15'), 83 83 }); 84 84 instance.prompt(); 85 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 85 + expect(instance.segmentCursor).to.deep.equal({ 86 + segmentIndex: 0, 87 + positionInSegment: 0, 88 + }); 86 89 // Move within year (0->1->2->3), then right from end goes to month 87 90 for (let i = 0; i < 4; i++) { 88 91 input.emit('keypress', undefined, { name: 'right' }); 89 92 } 90 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); 93 + expect(instance.segmentCursor).to.deep.equal({ 94 + segmentIndex: 1, 95 + positionInSegment: 0, 96 + }); 91 97 for (let i = 0; i < 2; i++) { 92 98 input.emit('keypress', undefined, { name: 'right' }); 93 99 } 94 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 2, positionInSegment: 0 }); 100 + expect(instance.segmentCursor).to.deep.equal({ 101 + segmentIndex: 2, 102 + positionInSegment: 0, 103 + }); 95 104 input.emit('keypress', undefined, { name: 'left' }); 96 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); 105 + expect(instance.segmentCursor).to.deep.equal({ 106 + segmentIndex: 1, 107 + positionInSegment: 0, 108 + }); 97 109 }); 98 110 99 111 test('up/down increments and decrements segment', () => { ··· 181 193 initialValue: d('2025-01-15'), 182 194 }); 183 195 instance.prompt(); 184 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 196 + expect(instance.segmentCursor).to.deep.equal({ 197 + segmentIndex: 0, 198 + positionInSegment: 0, 199 + }); 185 200 // Type 2,0,2,3 to change 2025 -> 2023 (edit digit by digit) 186 201 input.emit('keypress', '2', { name: undefined, sequence: '2' }); 187 202 input.emit('keypress', '0', { name: undefined, sequence: '0' }); 188 203 input.emit('keypress', '2', { name: undefined, sequence: '2' }); 189 204 input.emit('keypress', '3', { name: undefined, sequence: '3' }); 190 205 expect(instance.userInput).to.equal('2023/01/15'); 191 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 3 }); 206 + expect(instance.segmentCursor).to.deep.equal({ 207 + segmentIndex: 0, 208 + positionInSegment: 3, 209 + }); 192 210 }); 193 211 194 212 test('backspace clears entire segment at any cursor position', () => { ··· 201 219 }); 202 220 instance.prompt(); 203 221 expect(instance.userInput).to.equal('2025/12/21'); 204 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 222 + expect(instance.segmentCursor).to.deep.equal({ 223 + segmentIndex: 0, 224 + positionInSegment: 0, 225 + }); 205 226 // Backspace at first position clears whole year segment 206 227 input.emit('keypress', undefined, { name: 'backspace', sequence: '\x7f' }); 207 228 expect(instance.userInput).to.equal('____/12/21'); 208 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 229 + expect(instance.segmentCursor).to.deep.equal({ 230 + segmentIndex: 0, 231 + positionInSegment: 0, 232 + }); 209 233 }); 210 234 211 235 test('backspace clears segment when cursor at first char (2___)', () => { ··· 219 243 // Type "2" to get "2___" 220 244 input.emit('keypress', '2', { name: undefined, sequence: '2' }); 221 245 expect(instance.userInput).to.equal('2___/__/__'); 222 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 1 }); 246 + expect(instance.segmentCursor).to.deep.equal({ 247 + segmentIndex: 0, 248 + positionInSegment: 1, 249 + }); 223 250 // Move to first char (position 0) 224 251 input.emit('keypress', undefined, { name: 'left' }); 225 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 252 + expect(instance.segmentCursor).to.deep.equal({ 253 + segmentIndex: 0, 254 + positionInSegment: 0, 255 + }); 226 256 // Backspace should clear whole segment - also test char-based detection 227 257 input.emit('keypress', '\x7f', { name: undefined, sequence: '\x7f' }); 228 258 expect(instance.userInput).to.equal('____/__/__'); 229 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 }); 259 + expect(instance.segmentCursor).to.deep.equal({ 260 + segmentIndex: 0, 261 + positionInSegment: 0, 262 + }); 230 263 }); 231 264 232 265 test('digit input updates segment and jumps to next when complete', () => { ··· 242 275 input.emit('keypress', c, { name: undefined, sequence: c }); 243 276 } 244 277 expect(instance.userInput).to.equal('2025/__/__'); 245 - expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 }); 278 + expect(instance.segmentCursor).to.deep.equal({ 279 + segmentIndex: 1, 280 + positionInSegment: 0, 281 + }); 246 282 }); 247 283 248 284 test('submit returns ISO string for valid date', async () => {
+1 -1
packages/prompts/test/date.test.ts
··· 5 5 6 6 const d = (iso: string) => { 7 7 const [y, m, day] = iso.slice(0, 10).split('-').map(Number); 8 - return new Date(y, m - 1, day); 8 + return new Date(Date.UTC(y, m - 1, day)); 9 9 }; 10 10 11 11 describe.each(['true', 'false'])('date (isCI = %s)', (isCI) => {