@gjsify/cli 0.7.5 → 0.8.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/lib/cli-app.js CHANGED
@@ -19,7 +19,7 @@ import { readFileSync } from 'node:fs';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import yargs from 'yargs';
22
- import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, } from './commands/index.js';
22
+ import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, } from './commands/index.js';
23
23
  import { APP_NAME } from './constants.js';
24
24
  // Detect which runtime is executing the CLI (GJS or Node.js).
25
25
  // GJS MUST be checked first because @gjsify/process sets
@@ -106,6 +106,7 @@ export async function runCli(argv) {
106
106
  .command(barrels.command, barrels.description, barrels.builder, barrels.handler)
107
107
  .command(tsc.command, tsc.description, tsc.builder, tsc.handler)
108
108
  .command(affected.command, affected.description, affected.builder, affected.handler)
109
+ .command(storybook.command, storybook.description, storybook.builder, storybook.handler)
109
110
  .demandCommand(1)
110
111
  .epilogue(`Running on ${runtimeLabel()}`)
111
112
  .help()
@@ -30,3 +30,4 @@ export * from './upgrade.js';
30
30
  export * from './barrels.js';
31
31
  export * from './tsc.js';
32
32
  export * from './affected.js';
33
+ export * from './storybook.js';
@@ -30,3 +30,4 @@ export * from './upgrade.js';
30
30
  export * from './barrels.js';
31
31
  export * from './tsc.js';
32
32
  export * from './affected.js';
33
+ export * from './storybook.js';
@@ -21,7 +21,7 @@
21
21
  import { DEFAULT_REGISTRY, registryFor, whoami } from '@gjsify/npm-registry';
22
22
  import { loadNpmrc } from '../utils/load-npmrc.js';
23
23
  import { writeAuthToken } from '../utils/auth-npmrc.js';
24
- import { promptLine, promptHidden } from '../utils/prompt.js';
24
+ import { promptCredentials, promptLine } from '../utils/prompt.js';
25
25
  export const loginCommand = {
26
26
  command: 'login',
27
27
  description: "Log in to an npm registry (Node-free `npm login`). Prompts for username + password (hidden) and writes the token to ~/.npmrc. Use --otp for 2FA. Web-OAuth flow is not supported — this is npm's legacy credentials flow.",
@@ -44,12 +44,15 @@ export const loginCommand = {
44
44
  (args.scope ? registryFor(args.scope.startsWith('@') ? `${args.scope}/_` : `@${args.scope}/_`, npmrc) : undefined) ??
45
45
  DEFAULT_REGISTRY;
46
46
  const registryClean = registry.endsWith('/') ? registry : `${registry}/`;
47
- const username = args.username ?? (await promptLine(`Username: `));
47
+ // Read username (visible) + password (masked) in a single raw-mode TTY
48
+ // session — robust against cooked line-discipline state + the shared
49
+ // stdin resume/pause race that intermittently broke the old two-prompt
50
+ // sequence (Enter showing `^M`, or an empty username).
51
+ const { username, password } = await promptCredentials(args.username);
48
52
  if (!username) {
49
53
  console.error('gjsify login: a username is required.');
50
54
  process.exit(1);
51
55
  }
52
- const password = await promptHidden(`Password: `);
53
56
  if (!password) {
54
57
  console.error('gjsify login: a password is required.');
55
58
  process.exit(1);
@@ -0,0 +1,12 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface StorybookCliOptions {
3
+ stories?: string;
4
+ appId?: string;
5
+ title?: string;
6
+ globals?: string;
7
+ out?: string;
8
+ watch: boolean;
9
+ buildOnly: boolean;
10
+ }
11
+ export declare const storybookCommand: Command<unknown, StorybookCliOptions>;
12
+ export {};
@@ -0,0 +1,172 @@
1
+ // `gjsify storybook` — discover every *.story.ts in a project, generate a tiny
2
+ // entry that registers them, then build (`--app gjs`) and launch the generic
3
+ // @gjsify/storybook component browser. No per-project storybook app needed.
4
+ //
5
+ // Dispatch is IN-PROCESS (runCli + runGjsBundle), not a re-spawn of the CLI:
6
+ // the showcase-style `spawn(process.execPath, [cliBin, …])` pattern assumes
7
+ // Node, but under the committed GJS bundle `process.execPath` is `gjs` and the
8
+ // `../index.js` path is not a runnable entry. In-process dispatch works
9
+ // identically under Node and GJS. @gjsify/storybook is resolved from the
10
+ // CONSUMER's node_modules by the generated entry, so it stays out of the CLI's
11
+ // own dependencies.
12
+ import { spawn } from 'node:child_process';
13
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { join, relative, resolve } from 'node:path';
15
+ function readPackage(cwd) {
16
+ try {
17
+ return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /** Derive a stable GApplication id from the package name when none is configured. */
24
+ function deriveAppId(name) {
25
+ if (!name)
26
+ return 'org.gjsify.Storybook';
27
+ const slug = name
28
+ .replace(/^@/, '')
29
+ .replace(/[^a-zA-Z0-9]+/g, '.')
30
+ .replace(/^\.+|\.+$/g, '');
31
+ return `org.gjsify.storybook.${slug}`;
32
+ }
33
+ /** Recursively collect `*.story.{ts,js,mts,mjs}` files under a directory. */
34
+ function findStoryFiles(dir) {
35
+ const out = [];
36
+ if (!existsSync(dir))
37
+ return out;
38
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39
+ if (entry.name === 'node_modules' || entry.name.startsWith('.'))
40
+ continue;
41
+ const full = join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ out.push(...findStoryFiles(full));
44
+ }
45
+ else if (/\.story\.(ts|js|mts|mjs)$/.test(entry.name)) {
46
+ out.push(full);
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ function generateEntry(files, applicationId, title) {
52
+ const imports = files.map((file, i) => `import * as m${i} from ${JSON.stringify(file)};`).join('\n');
53
+ const namespaces = files.map((_, i) => `m${i}`).join(', ');
54
+ const titleLine = title ? `\n title: ${JSON.stringify(title)},` : '';
55
+ return `// Auto-generated by \`gjsify storybook\` — do not edit by hand.
56
+ import { collectStoryModules, runStorybook } from '@gjsify/storybook';
57
+ ${imports}
58
+
59
+ await runStorybook({
60
+ applicationId: ${JSON.stringify(applicationId)},${titleLine}
61
+ stories: collectStoryModules([${namespaces}]),
62
+ });
63
+ `;
64
+ }
65
+ export const storybookCommand = {
66
+ command: 'storybook',
67
+ description: 'Discover *.story.ts in this project and launch the GTK storybook browser.',
68
+ builder: (yargs) => yargs
69
+ .option('stories', {
70
+ description: 'Directory scanned recursively for *.story.ts (default: src, or gjsify.storybook.stories)',
71
+ type: 'string',
72
+ })
73
+ .option('app-id', {
74
+ description: 'GApplication id (default: gjsify.storybook.applicationId or derived from the package name)',
75
+ type: 'string',
76
+ })
77
+ .option('title', { description: 'Window title', type: 'string' })
78
+ .option('globals', {
79
+ description: 'Value for `gjsify build --globals` (default: auto; use auto,dom for canvas/DOM stories)',
80
+ type: 'string',
81
+ })
82
+ .option('out', {
83
+ description: 'Output bundle path (default: node_modules/.cache/gjsify-storybook)',
84
+ type: 'string',
85
+ })
86
+ .option('watch', {
87
+ description: 'Rebuild and relaunch when a story file changes',
88
+ type: 'boolean',
89
+ default: false,
90
+ })
91
+ .option('build-only', { description: 'Build the bundle but do not launch it', type: 'boolean', default: false }),
92
+ handler: async (args) => {
93
+ const cwd = process.cwd();
94
+ const pkg = readPackage(cwd);
95
+ const config = pkg.gjsify?.storybook ?? {};
96
+ const storiesDir = resolve(cwd, args.stories ?? config.stories ?? 'src');
97
+ const applicationId = args.appId ?? config.applicationId ?? deriveAppId(pkg.name);
98
+ const title = args.title ?? config.title;
99
+ const globals = args.globals ?? config.globals ?? 'auto';
100
+ const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify-storybook');
101
+ mkdirSync(cacheDir, { recursive: true });
102
+ const entryPath = join(cacheDir, 'entry.ts');
103
+ const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, 'storybook.gjs.mjs');
104
+ const writeEntry = () => {
105
+ const files = findStoryFiles(storiesDir).sort();
106
+ writeFileSync(entryPath, generateEntry(files, applicationId, title));
107
+ return files.length;
108
+ };
109
+ const initialCount = writeEntry();
110
+ if (initialCount === 0) {
111
+ console.error(`No *.story.ts files found under ${relative(cwd, storiesDir) || storiesDir}`);
112
+ process.exit(1);
113
+ }
114
+ console.log(`Found ${initialCount} story file(s) under ${relative(cwd, storiesDir) || '.'}`);
115
+ // In-process build — runtime-agnostic (Node uses npm rolldown, GJS uses
116
+ // the @gjsify/rolldown-native facade), no re-spawn of the CLI binary.
117
+ const { runCli } = await import('../cli-app.js');
118
+ const buildBundle = () => runCli(['build', entryPath, '--app', 'gjs', '--globals', globals, '--outfile', outPath]);
119
+ if (!args.watch) {
120
+ await buildBundle();
121
+ if (args.buildOnly)
122
+ return;
123
+ const { runGjsBundle } = await import('../utils/run-gjs.js');
124
+ await runGjsBundle(outPath, [], { exitOnSuccess: true });
125
+ return;
126
+ }
127
+ // Watch mode: rebuild + relaunch on any change under the stories dir.
128
+ // The storybook bundle only needs system Gtk/Adw typelibs at runtime
129
+ // (no @gjsify native bridges), so a plain `gjs -m` child is enough and
130
+ // stays killable for relaunch.
131
+ const { watch } = await import('node:fs');
132
+ let child = null;
133
+ let rebuilding = false;
134
+ let pending = false;
135
+ const rebuild = async () => {
136
+ if (rebuilding) {
137
+ pending = true;
138
+ return;
139
+ }
140
+ rebuilding = true;
141
+ try {
142
+ writeEntry();
143
+ if (child) {
144
+ child.kill();
145
+ child = null;
146
+ }
147
+ await buildBundle();
148
+ if (!args.buildOnly) {
149
+ child = spawn('gjs', ['-m', outPath], { stdio: 'inherit' });
150
+ }
151
+ }
152
+ catch (err) {
153
+ console.error(`[storybook] rebuild failed: ${err.message}`);
154
+ }
155
+ finally {
156
+ rebuilding = false;
157
+ if (pending) {
158
+ pending = false;
159
+ void rebuild();
160
+ }
161
+ }
162
+ };
163
+ await rebuild();
164
+ let debounce = null;
165
+ watch(storiesDir, { recursive: true }, () => {
166
+ if (debounce)
167
+ clearTimeout(debounce);
168
+ debounce = setTimeout(() => void rebuild(), 200);
169
+ });
170
+ console.log(`[storybook] watching ${relative(cwd, storiesDir) || '.'} — press Ctrl+C to stop`);
171
+ },
172
+ };
@@ -1,8 +1,34 @@
1
- /** Print a question and read one line from stdin (visible). */
2
- export declare function promptLine(question: string): Promise<string>;
1
+ /** Credentials read from the terminal. */
2
+ export interface PromptedCredentials {
3
+ username: string;
4
+ password: string;
5
+ }
6
+ /** Result of feeding one keystroke to the raw-mode line editor. */
7
+ export interface KeyOutcome {
8
+ /** The line buffer after this key. */
9
+ buf: string;
10
+ /** Text to echo to the terminal for this key (`''` = nothing). */
11
+ echo: string;
12
+ /** The line is complete (Enter). */
13
+ done: boolean;
14
+ /** Ctrl-C was pressed (caller should restore + exit). */
15
+ interrupt: boolean;
16
+ }
3
17
  /**
4
- * Print a question and read one line WITHOUT echoing it (passwords). Uses raw
5
- * mode + manual key handling on a TTY; falls back to a plain line read when
6
- * stdin is not a TTY (piped). Ctrl-C aborts the process.
18
+ * Pure key handler for the raw-mode line editor — the single source of truth
19
+ * for how a keystroke updates the buffer + echo. Kept pure (no I/O) so it is
20
+ * unit-testable: this is what makes Enter (`\r` OR `\n`) reliably submit, masks
21
+ * passwords, and treats Ctrl-C as an interrupt rather than text.
7
22
  */
23
+ export declare function applyKey(buf: string, ch: string, mask: boolean): KeyOutcome;
24
+ /** Print a question and read one visible line. */
25
+ export declare function promptLine(question: string): Promise<string>;
26
+ /** Print a question and read one line WITHOUT echoing it (passwords). */
8
27
  export declare function promptHidden(question: string): Promise<string>;
28
+ /**
29
+ * Read npm credentials. On a TTY both the visible username and the masked
30
+ * password are read in ONE raw session (no cooked-line dependency, no
31
+ * inter-prompt resume/pause race). `providedUsername` (from `--username`) skips
32
+ * the username prompt.
33
+ */
34
+ export declare function promptCredentials(providedUsername?: string): Promise<PromptedCredentials>;
@@ -1,81 +1,118 @@
1
- // Minimal interactive prompts for `gjsify login` — a visible line prompt and a
2
- // hidden (no-echo) password prompt. Cross-runtime: `process.stdin.setRawMode`
3
- // is provided by Node and by `@gjsify/process` (terminal-native) under GJS.
1
+ // Interactive prompts for `gjsify login` / `gjsify trust`.
4
2
  //
5
- // Non-TTY stdin (piped input, CI) is supported: both helpers read a single line
6
- // without raw-mode masking, so `printf 'user\npass\n' | gjsify login` works for
7
- // automation.
8
- const CTRL_C = String.fromCharCode(3); // ETX (Ctrl-C)
9
- const DEL = String.fromCharCode(127); // DEL (Backspace on most terminals)
10
- const BACKSPACE = String.fromCharCode(8); // BS
11
- /** Print a question and read one line from stdin (visible). */
12
- export async function promptLine(question) {
13
- process.stdout.write(question);
14
- return readLine();
3
+ // On a TTY every prompt runs inside a single RAW-mode session (`runRawSession`)
4
+ // that reads key-by-key with manual echo and a guaranteed cooked-mode restore
5
+ // (try/finally). This deliberately does NOT rely on the terminal's cooked
6
+ // line-discipline nor on the shared `process.stdin` resume/pause cycle:
7
+ // - The old visible prompt read a cooked line via `process.stdin` 'data'
8
+ // events. With the line discipline in an unexpected state (e.g. ICRNL off
9
+ // after a prior raw prompt) Enter arrived as a bare `\r` that never
10
+ // terminated the cooked line — the prompt hung showing `name^M`. The
11
+ // resume/pause churn on the shared stdin singleton between two sequential
12
+ // prompts also intermittently dropped the line or resolved it empty.
13
+ // - Reading raw + handling `\r`/`\n` ourselves removes both failure modes.
14
+ //
15
+ // Raw mode (via @gjsify/process → terminal-native) now also clears ISIG, so
16
+ // Ctrl-C is delivered as a `\x03` keystroke we handle (restore + exit) instead
17
+ // of a SIGINT that would kill the process leaving the terminal in raw mode.
18
+ //
19
+ // Non-TTY stdin (piped input, CI) keeps a plain line read so
20
+ // `printf 'user\npass\n' | gjsify login` still works.
21
+ const CTRL_C = '\x03'; // ETX (Ctrl-C)
22
+ const DEL = '\x7f'; // DEL (Backspace on most terminals)
23
+ const BACKSPACE = '\x08'; // BS
24
+ function isTtyStdin() {
25
+ const stdin = process.stdin;
26
+ return Boolean(stdin.isTTY) && typeof stdin.setRawMode === 'function';
15
27
  }
16
28
  /**
17
- * Print a question and read one line WITHOUT echoing it (passwords). Uses raw
18
- * mode + manual key handling on a TTY; falls back to a plain line read when
19
- * stdin is not a TTY (piped). Ctrl-C aborts the process.
29
+ * Pure key handler for the raw-mode line editor — the single source of truth
30
+ * for how a keystroke updates the buffer + echo. Kept pure (no I/O) so it is
31
+ * unit-testable: this is what makes Enter (`\r` OR `\n`) reliably submit, masks
32
+ * passwords, and treats Ctrl-C as an interrupt rather than text.
20
33
  */
21
- export async function promptHidden(question) {
22
- process.stdout.write(question);
23
- const stdin = process.stdin;
24
- if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
25
- // Non-interactive: read a line as-is (no masking possible/needed).
26
- return readLine();
34
+ export function applyKey(buf, ch, mask) {
35
+ if (ch === '\r' || ch === '\n') {
36
+ return { buf, echo: '\n', done: true, interrupt: false };
27
37
  }
28
- return new Promise((resolve) => {
29
- let buf = '';
30
- stdin.setRawMode(true);
31
- stdin.resume();
32
- // setEncoding makes 'data' emit decoded STRINGS — including under GJS,
33
- // where @gjsify/process honours it via StringDecoder (was a no-op stub;
34
- // bytes broke `ch === '\r'` so Enter never submitted). So we can iterate
35
- // characters directly here; do NOT reintroduce a byte-normalising shim.
36
- stdin.setEncoding('utf-8');
37
- const onData = (chunk) => {
38
- for (const ch of chunk) {
39
- if (ch === '\r' || ch === '\n') {
40
- cleanup();
41
- process.stdout.write('\n');
42
- resolve(buf);
43
- return;
44
- }
45
- else if (ch === CTRL_C) {
46
- cleanup();
47
- process.stdout.write('\n');
48
- process.exit(130);
49
- }
50
- else if (ch === DEL || ch === BACKSPACE) {
51
- if (buf.length > 0) {
52
- buf = buf.slice(0, -1);
53
- process.stdout.write('\b \b');
54
- }
55
- }
56
- else if (ch >= ' ') {
57
- buf += ch;
58
- process.stdout.write('*');
59
- }
60
- }
61
- };
62
- const cleanup = () => {
38
+ if (ch === CTRL_C) {
39
+ return { buf, echo: '\n', done: false, interrupt: true };
40
+ }
41
+ if (ch === DEL || ch === BACKSPACE) {
42
+ if (buf.length > 0) {
43
+ return { buf: buf.slice(0, -1), echo: '\b \b', done: false, interrupt: false };
44
+ }
45
+ return { buf, echo: '', done: false, interrupt: false };
46
+ }
47
+ if (ch >= ' ') {
48
+ return { buf: buf + ch, echo: mask ? '*' : ch, done: false, interrupt: false };
49
+ }
50
+ // Other control characters (arrows, tabs, escapes) are ignored.
51
+ return { buf, echo: '', done: false, interrupt: false };
52
+ }
53
+ /**
54
+ * Open ONE raw-mode stdin session, run `fn` (which may read several lines via
55
+ * the supplied reader), and always restore cooked mode afterwards. Keeping a
56
+ * single session for the whole credential exchange avoids per-prompt
57
+ * resume/pause races on the shared stdin singleton.
58
+ */
59
+ async function runRawSession(fn) {
60
+ const stdin = process.stdin;
61
+ const out = process.stdout;
62
+ let pending = null;
63
+ const cleanup = () => {
64
+ stdin.removeListener('data', onData);
65
+ try {
63
66
  stdin.setRawMode(false);
64
- stdin.removeListener('data', onData);
67
+ }
68
+ catch {
69
+ /* not a TTY any more */
70
+ }
71
+ if (typeof stdin.pause === 'function')
65
72
  stdin.pause();
66
- };
67
- stdin.on('data', onData);
73
+ };
74
+ const onData = (chunk) => {
75
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
76
+ for (const ch of text) {
77
+ if (!pending)
78
+ continue; // ignore type-ahead between prompts
79
+ const r = applyKey(pending.buf, ch, pending.mask);
80
+ pending.buf = r.buf;
81
+ if (r.echo)
82
+ out.write(r.echo);
83
+ if (r.interrupt) {
84
+ cleanup();
85
+ process.exit(130);
86
+ }
87
+ if (r.done) {
88
+ const { resolve } = pending;
89
+ pending = null;
90
+ resolve(r.buf);
91
+ }
92
+ }
93
+ };
94
+ stdin.setRawMode(true);
95
+ if (typeof stdin.resume === 'function')
96
+ stdin.resume();
97
+ stdin.setEncoding('utf-8');
98
+ stdin.on('data', onData);
99
+ const read = (question, mask) => new Promise((resolve) => {
100
+ out.write(question);
101
+ pending = { mask, buf: '', resolve };
68
102
  });
103
+ try {
104
+ return await fn(read);
105
+ }
106
+ finally {
107
+ cleanup();
108
+ }
69
109
  }
70
- /** Read a single line from stdin (shared by both prompts on non-TTY). */
110
+ /** Read a single line from non-TTY stdin (piped). Accepts CR, LF, or CRLF. */
71
111
  function readLine() {
72
112
  return new Promise((resolve) => {
73
113
  let buf = '';
74
114
  const onData = (chunk) => {
75
115
  buf += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
76
- // Accept CR, LF, or CRLF as the line terminator. A bare CR (`\r`)
77
- // is what some terminals / the GJS terminal-native stdin deliver on
78
- // Enter — keying only on `\n` made the prompt hang forever there.
79
116
  const m = buf.search(/[\r\n]/);
80
117
  if (m >= 0) {
81
118
  cleanup();
@@ -97,3 +134,42 @@ function readLine() {
97
134
  process.stdin.once('end', onEnd);
98
135
  });
99
136
  }
137
+ /** Print a question and read one visible line. */
138
+ export async function promptLine(question) {
139
+ if (!isTtyStdin()) {
140
+ process.stdout.write(question);
141
+ return readLine();
142
+ }
143
+ return runRawSession((read) => read(question, false));
144
+ }
145
+ /** Print a question and read one line WITHOUT echoing it (passwords). */
146
+ export async function promptHidden(question) {
147
+ if (!isTtyStdin()) {
148
+ process.stdout.write(question);
149
+ return readLine();
150
+ }
151
+ return runRawSession((read) => read(question, true));
152
+ }
153
+ /**
154
+ * Read npm credentials. On a TTY both the visible username and the masked
155
+ * password are read in ONE raw session (no cooked-line dependency, no
156
+ * inter-prompt resume/pause race). `providedUsername` (from `--username`) skips
157
+ * the username prompt.
158
+ */
159
+ export async function promptCredentials(providedUsername) {
160
+ if (!isTtyStdin()) {
161
+ let username = providedUsername;
162
+ if (!username) {
163
+ process.stdout.write('Username: ');
164
+ username = await readLine();
165
+ }
166
+ process.stdout.write('Password: ');
167
+ const password = await readLine();
168
+ return { username, password };
169
+ }
170
+ return runRawSession(async (read) => {
171
+ const username = providedUsername ?? (await read('Username: ', false));
172
+ const password = await read('Password: ', true);
173
+ return { username, password };
174
+ });
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -120,19 +120,19 @@
120
120
  "cli"
121
121
  ],
122
122
  "dependencies": {
123
- "@gjsify/buffer": "^0.7.5",
124
- "@gjsify/create-app": "^0.7.5",
125
- "@gjsify/node-globals": "^0.7.5",
126
- "@gjsify/node-polyfills": "^0.7.5",
127
- "@gjsify/npm-registry": "^0.7.5",
128
- "@gjsify/resolve-npm": "^0.7.5",
129
- "@gjsify/rolldown-plugin-gjsify": "^0.7.5",
130
- "@gjsify/rolldown-plugin-pnp": "^0.7.5",
131
- "@gjsify/semver": "^0.7.5",
132
- "@gjsify/tar": "^0.7.5",
133
- "@gjsify/tsc": "^0.7.5",
134
- "@gjsify/web-polyfills": "^0.7.5",
135
- "@gjsify/workspace": "^0.7.5",
123
+ "@gjsify/buffer": "^0.8.0",
124
+ "@gjsify/create-app": "^0.8.0",
125
+ "@gjsify/node-globals": "^0.8.0",
126
+ "@gjsify/node-polyfills": "^0.8.0",
127
+ "@gjsify/npm-registry": "^0.8.0",
128
+ "@gjsify/resolve-npm": "^0.8.0",
129
+ "@gjsify/rolldown-plugin-gjsify": "^0.8.0",
130
+ "@gjsify/rolldown-plugin-pnp": "^0.8.0",
131
+ "@gjsify/semver": "^0.8.0",
132
+ "@gjsify/tar": "^0.8.0",
133
+ "@gjsify/tsc": "^0.8.0",
134
+ "@gjsify/web-polyfills": "^0.8.0",
135
+ "@gjsify/workspace": "^0.8.0",
136
136
  "cosmiconfig": "^9.0.2",
137
137
  "get-tsconfig": "^4.14.0",
138
138
  "pkg-types": "^2.3.1",
@@ -140,13 +140,13 @@
140
140
  "yargs": "^18.0.0"
141
141
  },
142
142
  "devDependencies": {
143
- "@gjsify/unit": "^0.7.5",
143
+ "@gjsify/unit": "^0.8.0",
144
144
  "@types/yargs": "^17.0.35",
145
145
  "typescript": "^6.0.3"
146
146
  },
147
147
  "peerDependencies": {
148
- "@gjsify/oxfmt-native": "^0.7.5",
149
- "@gjsify/rolldown-native": "^0.7.5"
148
+ "@gjsify/oxfmt-native": "^0.8.0",
149
+ "@gjsify/rolldown-native": "^0.8.0"
150
150
  },
151
151
  "peerDependenciesMeta": {
152
152
  "@gjsify/oxfmt-native": {