fullstackgtm 0.55.0 → 0.55.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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.55.1] — 2026-07-13
11
+
12
+ ### Fixed
13
+
14
+ - Interactive masked login prompts no longer repaint an empty
15
+ `0 characters received` line after Enter is pressed. Submission now settles
16
+ the renderer before `readline` clears its internal input buffer.
17
+
10
18
  ## [0.55.0] — 2026-07-13
11
19
 
12
20
  ### Added
@@ -65,6 +65,15 @@ export declare function loadIcp(args: string[]): Icp | undefined;
65
65
  * e.g. `echo "$TOKEN" | fullstackgtm login hubspot`
66
66
  * - interactive: prompted on the TTY with the input muted
67
67
  */
68
+ export declare function createSecretMaskRenderer(options: {
69
+ label: string;
70
+ lineLength: () => number;
71
+ write: (value: string) => void;
72
+ }): {
73
+ mute(): void;
74
+ settle(): void;
75
+ write(value: string): void;
76
+ };
68
77
  export declare function readSecret(label: string): Promise<string>;
69
78
  export declare function isOptionValue(args: string[], arg: string): boolean;
70
79
  export declare function readPackageInfo(): {
@@ -288,6 +288,35 @@ export function loadIcp(args) {
288
288
  * e.g. `echo "$TOKEN" | fullstackgtm login hubspot`
289
289
  * - interactive: prompted on the TTY with the input muted
290
290
  */
291
+ export function createSecretMaskRenderer(options) {
292
+ let muted = false;
293
+ let renderQueued = false;
294
+ let settled = false;
295
+ return {
296
+ mute() { muted = true; },
297
+ settle() { settled = true; },
298
+ write(value) {
299
+ if (!muted) {
300
+ options.write(value);
301
+ return;
302
+ }
303
+ if (settled || renderQueued)
304
+ return;
305
+ renderQueued = true;
306
+ queueMicrotask(() => {
307
+ renderQueued = false;
308
+ // readline clears `line` during close. Submission settles this renderer
309
+ // first so an Enter-triggered repaint cannot redraw an empty prompt
310
+ // after the final mask/newline ("0 characters received").
311
+ if (settled)
312
+ return;
313
+ const length = options.lineLength();
314
+ const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
315
+ options.write(`\r\u001b[2K${options.label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
316
+ });
317
+ },
318
+ };
319
+ }
291
320
  export async function readSecret(label) {
292
321
  if (!process.stdin.isTTY) {
293
322
  const chunks = [];
@@ -311,30 +340,20 @@ export async function readSecret(label) {
311
340
  output: process.stderr,
312
341
  terminal: true,
313
342
  });
314
- let muted = false;
315
343
  const mutable = rl;
316
- let renderQueued = false;
317
- mutable._writeToOutput = (value) => {
318
- if (!muted) {
319
- process.stderr.write(value);
320
- return;
321
- }
322
- if (renderQueued)
323
- return;
324
- renderQueued = true;
325
- queueMicrotask(() => {
326
- renderQueued = false;
327
- const length = mutable.line?.length ?? 0;
328
- const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
329
- process.stderr.write(`\r\u001b[2K${label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
330
- });
331
- };
344
+ const renderer = createSecretMaskRenderer({
345
+ label,
346
+ lineLength: () => mutable.line?.length ?? 0,
347
+ write: (value) => process.stderr.write(value),
348
+ });
349
+ mutable._writeToOutput = (value) => renderer.write(value);
332
350
  rl.question(`${label}: `, (answer) => {
351
+ renderer.settle();
333
352
  rl.close();
334
353
  process.stderr.write("\n");
335
354
  resolveSecret(answer.trim());
336
355
  });
337
- muted = true;
356
+ renderer.mute();
338
357
  });
339
358
  }
340
359
  export function isOptionValue(args, arg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
package/src/cli/shared.ts CHANGED
@@ -336,6 +336,35 @@ export function loadIcp(args: string[]): Icp | undefined {
336
336
  * e.g. `echo "$TOKEN" | fullstackgtm login hubspot`
337
337
  * - interactive: prompted on the TTY with the input muted
338
338
  */
339
+ export function createSecretMaskRenderer(options: {
340
+ label: string;
341
+ lineLength: () => number;
342
+ write: (value: string) => void;
343
+ }) {
344
+ let muted = false;
345
+ let renderQueued = false;
346
+ let settled = false;
347
+ return {
348
+ mute() { muted = true; },
349
+ settle() { settled = true; },
350
+ write(value: string) {
351
+ if (!muted) { options.write(value); return; }
352
+ if (settled || renderQueued) return;
353
+ renderQueued = true;
354
+ queueMicrotask(() => {
355
+ renderQueued = false;
356
+ // readline clears `line` during close. Submission settles this renderer
357
+ // first so an Enter-triggered repaint cannot redraw an empty prompt
358
+ // after the final mask/newline ("0 characters received").
359
+ if (settled) return;
360
+ const length = options.lineLength();
361
+ const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
362
+ options.write(`\r\u001b[2K${options.label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
363
+ });
364
+ },
365
+ };
366
+ }
367
+
339
368
  export async function readSecret(label: string): Promise<string> {
340
369
  if (!process.stdin.isTTY) {
341
370
  const chunks: Buffer[] = [];
@@ -362,26 +391,20 @@ export async function readSecret(label: string): Promise<string> {
362
391
  output: process.stderr,
363
392
  terminal: true,
364
393
  });
365
- let muted = false;
366
394
  const mutable = rl as unknown as { _writeToOutput?: (value: string) => void; line?: string };
367
- let renderQueued = false;
368
- mutable._writeToOutput = (value: string) => {
369
- if (!muted) { process.stderr.write(value); return; }
370
- if (renderQueued) return;
371
- renderQueued = true;
372
- queueMicrotask(() => {
373
- renderQueued = false;
374
- const length = mutable.line?.length ?? 0;
375
- const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
376
- process.stderr.write(`\r\u001b[2K${label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
377
- });
378
- };
395
+ const renderer = createSecretMaskRenderer({
396
+ label,
397
+ lineLength: () => mutable.line?.length ?? 0,
398
+ write: (value) => process.stderr.write(value),
399
+ });
400
+ mutable._writeToOutput = (value: string) => renderer.write(value);
379
401
  rl.question(`${label}: `, (answer) => {
402
+ renderer.settle();
380
403
  rl.close();
381
404
  process.stderr.write("\n");
382
405
  resolveSecret(answer.trim());
383
406
  });
384
- muted = true;
407
+ renderer.mute();
385
408
  });
386
409
  }
387
410