@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/dist/cli.gjs.mjs +46 -37
- package/lib/cli-app.js +2 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/login.js +6 -3
- package/lib/commands/storybook.d.ts +12 -0
- package/lib/commands/storybook.js +172 -0
- package/lib/utils/prompt.d.ts +31 -5
- package/lib/utils/prompt.js +140 -64
- package/package.json +17 -17
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()
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
package/lib/commands/login.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/utils/prompt.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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>;
|
package/lib/utils/prompt.js
CHANGED
|
@@ -1,81 +1,118 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* not a TTY any more */
|
|
70
|
+
}
|
|
71
|
+
if (typeof stdin.pause === 'function')
|
|
65
72
|
stdin.pause();
|
|
66
|
-
|
|
67
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
124
|
-
"@gjsify/create-app": "^0.
|
|
125
|
-
"@gjsify/node-globals": "^0.
|
|
126
|
-
"@gjsify/node-polyfills": "^0.
|
|
127
|
-
"@gjsify/npm-registry": "^0.
|
|
128
|
-
"@gjsify/resolve-npm": "^0.
|
|
129
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
130
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
131
|
-
"@gjsify/semver": "^0.
|
|
132
|
-
"@gjsify/tar": "^0.
|
|
133
|
-
"@gjsify/tsc": "^0.
|
|
134
|
-
"@gjsify/web-polyfills": "^0.
|
|
135
|
-
"@gjsify/workspace": "^0.
|
|
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.
|
|
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.
|
|
149
|
-
"@gjsify/rolldown-native": "^0.
|
|
148
|
+
"@gjsify/oxfmt-native": "^0.8.0",
|
|
149
|
+
"@gjsify/rolldown-native": "^0.8.0"
|
|
150
150
|
},
|
|
151
151
|
"peerDependenciesMeta": {
|
|
152
152
|
"@gjsify/oxfmt-native": {
|