@gjsify/cli 0.7.5 → 0.9.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 +91 -82
- package/lib/actions/build.d.ts +31 -0
- package/lib/actions/build.js +135 -3
- package/lib/bundler-pick.js +6 -9
- package/lib/cli-app.js +4 -2
- 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/run.js +2 -6
- package/lib/commands/storybook.d.ts +12 -0
- package/lib/commands/storybook.js +172 -0
- package/lib/utils/oxc-resolve.js +3 -4
- package/lib/utils/prompt.d.ts +31 -5
- package/lib/utils/prompt.js +140 -64
- package/lib/utils/resolve-plugin-by-name.d.ts +21 -1
- package/lib/utils/resolve-plugin-by-name.js +5 -2
- package/package.json +17 -17
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
|
+
}
|
|
@@ -7,6 +7,26 @@ export interface PluginByName {
|
|
|
7
7
|
/** Options forwarded to the plugin factory. */
|
|
8
8
|
options?: unknown;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Strategy for turning a resolved plugin file path into its module namespace.
|
|
12
|
+
*
|
|
13
|
+
* On Node this is a plain dynamic `import()`. Under GJS it cannot be — GJS's
|
|
14
|
+
* native ESM loader does not follow npm `package.json#exports` subpath maps
|
|
15
|
+
* for bare specifiers, so importing a plugin whose source does
|
|
16
|
+
* `import … from '@scope/pkg/internal-subpath'` throws
|
|
17
|
+
* `Module not found: @scope/pkg/internal-subpath` even when the file is on
|
|
18
|
+
* disk. The CLI injects a GJS strategy that first bundles the plugin to a
|
|
19
|
+
* single self-contained ESM file (Rolldown resolves the exports map at
|
|
20
|
+
* bundle time), then imports that. See `BuildAction.loadPluginViaGjsBundle`.
|
|
21
|
+
*/
|
|
22
|
+
export type LoadPluginModule = (resolvedPath: string, pluginName: string) => Promise<Record<string, unknown>>;
|
|
23
|
+
export interface ResolveUserPluginsOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Override how a resolved plugin path is turned into its module
|
|
26
|
+
* namespace. Defaults to a direct dynamic `import()` (correct on Node).
|
|
27
|
+
*/
|
|
28
|
+
loadModule?: LoadPluginModule;
|
|
29
|
+
}
|
|
10
30
|
/** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
|
|
11
31
|
export declare function isPluginByName(value: unknown): value is PluginByName;
|
|
12
32
|
/**
|
|
@@ -18,4 +38,4 @@ export declare function isPluginByName(value: unknown): value is PluginByName;
|
|
|
18
38
|
* Throws when a name fails to resolve, when the chosen export is not a
|
|
19
39
|
* function, or when the factory returns nothing.
|
|
20
40
|
*/
|
|
21
|
-
export declare function resolveUserPlugins(plugins: ReadonlyArray<RolldownPluginOption | PluginByName>, projectDir: string): Promise<RolldownPluginOption[]>;
|
|
41
|
+
export declare function resolveUserPlugins(plugins: ReadonlyArray<RolldownPluginOption | PluginByName>, projectDir: string, options?: ResolveUserPluginsOptions): Promise<RolldownPluginOption[]>;
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { pathToFileURL } from 'node:url';
|
|
18
|
+
/** Default loader: a plain dynamic import of the resolved file URL (Node path). */
|
|
19
|
+
const defaultLoadModule = async (resolvedPath) => (await import(pathToFileURL(resolvedPath).href));
|
|
18
20
|
/** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
|
|
19
21
|
export function isPluginByName(value) {
|
|
20
22
|
return (typeof value === 'object' &&
|
|
@@ -39,7 +41,8 @@ export function isPluginByName(value) {
|
|
|
39
41
|
* Throws when a name fails to resolve, when the chosen export is not a
|
|
40
42
|
* function, or when the factory returns nothing.
|
|
41
43
|
*/
|
|
42
|
-
export async function resolveUserPlugins(plugins, projectDir) {
|
|
44
|
+
export async function resolveUserPlugins(plugins, projectDir, options = {}) {
|
|
45
|
+
const loadModule = options.loadModule ?? defaultLoadModule;
|
|
43
46
|
const requireFromProject = createRequire(join(projectDir, 'package.json'));
|
|
44
47
|
const out = [];
|
|
45
48
|
for (const entry of plugins) {
|
|
@@ -56,7 +59,7 @@ export async function resolveUserPlugins(plugins, projectDir) {
|
|
|
56
59
|
`Add it to your project's dependencies, or pass a Plugin object directly. ` +
|
|
57
60
|
`(${err.message})`);
|
|
58
61
|
}
|
|
59
|
-
const mod = await
|
|
62
|
+
const mod = await loadModule(resolvedPath, entry.name);
|
|
60
63
|
const exportName = entry.export ?? 'default';
|
|
61
64
|
const factory = mod[exportName];
|
|
62
65
|
if (typeof factory !== 'function') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.9.0",
|
|
124
|
+
"@gjsify/create-app": "^0.9.0",
|
|
125
|
+
"@gjsify/node-globals": "^0.9.0",
|
|
126
|
+
"@gjsify/node-polyfills": "^0.9.0",
|
|
127
|
+
"@gjsify/npm-registry": "^0.9.0",
|
|
128
|
+
"@gjsify/resolve-npm": "^0.9.0",
|
|
129
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.9.0",
|
|
130
|
+
"@gjsify/rolldown-plugin-pnp": "^0.9.0",
|
|
131
|
+
"@gjsify/semver": "^0.9.0",
|
|
132
|
+
"@gjsify/tar": "^0.9.0",
|
|
133
|
+
"@gjsify/tsc": "^0.9.0",
|
|
134
|
+
"@gjsify/web-polyfills": "^0.9.0",
|
|
135
|
+
"@gjsify/workspace": "^0.9.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.9.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.9.0",
|
|
149
|
+
"@gjsify/rolldown-native": "^0.9.0"
|
|
150
150
|
},
|
|
151
151
|
"peerDependenciesMeta": {
|
|
152
152
|
"@gjsify/oxfmt-native": {
|