@gjsify/cli 0.4.44 → 0.5.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/dist/cli.gjs.mjs +214 -215
- package/lib/actions/build.js +9 -1
- package/lib/bundler-pick.d.ts +56 -0
- package/lib/bundler-pick.js +153 -16
- package/lib/cli-app.d.ts +10 -0
- package/lib/cli-app.js +112 -0
- package/lib/commands/affected.js +4 -0
- package/lib/commands/build.js +5 -1
- package/lib/commands/check.js +21 -13
- package/lib/commands/dlx.js +3 -1
- package/lib/commands/fix.js +3 -3
- package/lib/commands/foreach.js +269 -31
- package/lib/commands/format.js +3 -3
- package/lib/commands/install.js +68 -16
- package/lib/commands/lint.js +3 -3
- package/lib/commands/publish.js +37 -24
- package/lib/commands/run.js +132 -2
- package/lib/commands/test.js +5 -0
- package/lib/commands/tsc.js +78 -24
- package/lib/index.js +7 -96
- package/lib/utils/npm-oidc.d.ts +23 -1
- package/lib/utils/npm-oidc.js +44 -6
- package/lib/utils/oxc-resolve.d.ts +36 -5
- package/lib/utils/oxc-resolve.js +132 -6
- package/lib/utils/prefixed-output.d.ts +7 -0
- package/lib/utils/prefixed-output.js +52 -0
- package/lib/utils/publish-headers.d.ts +45 -0
- package/lib/utils/publish-headers.js +99 -0
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +8 -7
- package/package.json +22 -18
package/lib/commands/run.js
CHANGED
|
@@ -33,10 +33,25 @@ export const runCommand = {
|
|
|
33
33
|
type: 'string',
|
|
34
34
|
array: true,
|
|
35
35
|
default: [],
|
|
36
|
+
})
|
|
37
|
+
.parserConfiguration({
|
|
38
|
+
// Preserve `--` as args['--'] so callers can write
|
|
39
|
+
// gjsify run ./server.mjs -- --port 8080
|
|
40
|
+
// without yargs intercepting --port as its own option.
|
|
41
|
+
// Without this, anything after `--` lands in args._ and is
|
|
42
|
+
// silently dropped by the handler.
|
|
43
|
+
'populate--': true,
|
|
36
44
|
}),
|
|
37
45
|
handler: async (args) => {
|
|
38
46
|
const target = args.target;
|
|
39
|
-
|
|
47
|
+
// Collect positional args AND anything after the `--` separator.
|
|
48
|
+
// With `parserConfiguration({ 'populate--': true })` in the builder,
|
|
49
|
+
// `gjsify run target -- a b c` puts a, b, c into args['--'] rather
|
|
50
|
+
// than silently discarding them (without it they land in args._ and
|
|
51
|
+
// are never forwarded to the child).
|
|
52
|
+
const positionalArgs = args.args ?? [];
|
|
53
|
+
const doubleDashArgs = (args['--'] ?? []).filter((v) => typeof v === 'string');
|
|
54
|
+
const extraArgs = [...positionalArgs, ...doubleDashArgs];
|
|
40
55
|
// Script lookup wins over file detection. Without this, a bare
|
|
41
56
|
// `gjsify run build` would resolve to a file when a `./build`
|
|
42
57
|
// directory exists in cwd (e.g. meson's build dir from a Vala
|
|
@@ -47,7 +62,9 @@ export const runCommand = {
|
|
|
47
62
|
const hasScript = pkg?.scripts && typeof pkg.scripts[target] === 'string';
|
|
48
63
|
if (!hasScript && looksLikeFile(target)) {
|
|
49
64
|
const file = resolve(target);
|
|
50
|
-
|
|
65
|
+
// Terminal call — exit on success, or the GJS main loop parks
|
|
66
|
+
// this process forever (see RunGjsBundleOptions.exitOnSuccess).
|
|
67
|
+
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
51
68
|
return;
|
|
52
69
|
}
|
|
53
70
|
await runScript(target, extraArgs);
|
|
@@ -117,6 +134,43 @@ async function runScript(script, extraArgs) {
|
|
|
117
134
|
npm_package_name: pkg.name ?? '',
|
|
118
135
|
npm_package_version: pkg.version ?? '',
|
|
119
136
|
};
|
|
137
|
+
// In-process fast path: under GJS, the build orchestration chains a
|
|
138
|
+
// heavyweight gjs per level (`foreach → npm → gjsify run X → gjsify <cmd>`),
|
|
139
|
+
// which oversubscribes CI's few cores and thrashes. When the script is a
|
|
140
|
+
// single `gjsify <subcommand>` (no shell operators / substitutions /
|
|
141
|
+
// unquoted globs), dispatch it through the same yargs surface IN THIS
|
|
142
|
+
// process instead of spawning another gjs — collapsing two gjs into one.
|
|
143
|
+
// Anything fancier (compound `&&`, pipes, a non-gjsify command) falls
|
|
144
|
+
// through to the shell spawn below. Node keeps spawning (cheap there); this
|
|
145
|
+
// only triggers under GJS, where the nesting actually hurts. See cli-app.ts.
|
|
146
|
+
const inProcArgv = runningUnderGjs() ? gjsifyInProcessArgv(literal, extraArgs) : null;
|
|
147
|
+
if (inProcArgv) {
|
|
148
|
+
// The subcommand runs in our process, so surface the script env on
|
|
149
|
+
// `process.env` (PATH with the workspace .bin dirs, npm_* lifecycle
|
|
150
|
+
// vars) exactly as the spawn path would. We process.exit afterwards, so
|
|
151
|
+
// mutating the global env is fine.
|
|
152
|
+
process.env.PATH = env.PATH;
|
|
153
|
+
process.env.npm_lifecycle_event = env.npm_lifecycle_event;
|
|
154
|
+
process.env.npm_package_name = env.npm_package_name;
|
|
155
|
+
process.env.npm_package_version = env.npm_package_version;
|
|
156
|
+
if (env.FORCE_COLOR !== undefined)
|
|
157
|
+
process.env.FORCE_COLOR = env.FORCE_COLOR;
|
|
158
|
+
try {
|
|
159
|
+
// Dynamic import to avoid a static cli-app ↔ run.ts import cycle;
|
|
160
|
+
// cli-app is already loaded (the CLI is running through it), so this
|
|
161
|
+
// resolves from cache instantly.
|
|
162
|
+
const { runCli } = await import('../cli-app.js');
|
|
163
|
+
await runCli(inProcArgv);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error(err.message);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
// Honor a non-zero `process.exitCode` set by the dispatched handler
|
|
170
|
+
// (e.g. a failing check that sets the code instead of exiting) —
|
|
171
|
+
// a bare exit(0) here masked such failures as success.
|
|
172
|
+
process.exit(process.exitCode != null ? Number(process.exitCode) : 0);
|
|
173
|
+
}
|
|
120
174
|
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
121
175
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
122
176
|
// child exits — without an explicit process.exit() the success path
|
|
@@ -141,3 +195,79 @@ function shellEscape(arg) {
|
|
|
141
195
|
return arg;
|
|
142
196
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
143
197
|
}
|
|
198
|
+
// Detected at CALL time (a module-load const reads `undefined` in the bundled
|
|
199
|
+
// `--app gjs` form). `bundler-pick.ts` uses the same expression.
|
|
200
|
+
function runningUnderGjs() {
|
|
201
|
+
return typeof globalThis.imports?.gi !== 'undefined';
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* If `literal` is a SINGLE `gjsify <subcommand> …` command with no shell
|
|
205
|
+
* operators / substitutions / unquoted globs, return the argv to feed `runCli`
|
|
206
|
+
* (the subcommand + its args, with `extraArgs` appended). Otherwise null — the
|
|
207
|
+
* caller falls back to the shell spawn path. Quotes are honoured so a quoted
|
|
208
|
+
* glob (`'src/**'`) survives as a single literal token, exactly as the shell
|
|
209
|
+
* would hand it to `gjsify` (which does its own glob expansion).
|
|
210
|
+
*/
|
|
211
|
+
function gjsifyInProcessArgv(literal, extraArgs) {
|
|
212
|
+
const tokens = tokenizeSimpleCommand(literal);
|
|
213
|
+
if (!tokens || tokens.length < 2 || tokens[0] !== 'gjsify')
|
|
214
|
+
return null;
|
|
215
|
+
return [...tokens.slice(1), ...extraArgs];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Quote-aware tokenizer for a SIMPLE command line (no shell features beyond
|
|
219
|
+
* single/double quoting). Returns the tokens, or null if the string contains
|
|
220
|
+
* anything the shell would treat specially — operators (`&& | ; < > &`),
|
|
221
|
+
* substitutions (`$(...)` / backticks / `$VAR`), unquoted globs/expansions
|
|
222
|
+
* (`* ? { } [ ] ~`), comments (`#`), or an unterminated quote — in which case
|
|
223
|
+
* the caller MUST use the real shell instead.
|
|
224
|
+
*/
|
|
225
|
+
function tokenizeSimpleCommand(cmd) {
|
|
226
|
+
const tokens = [];
|
|
227
|
+
let cur = '';
|
|
228
|
+
let has = false;
|
|
229
|
+
let quote = null;
|
|
230
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
231
|
+
const c = cmd[i];
|
|
232
|
+
if (quote === "'") {
|
|
233
|
+
if (c === "'")
|
|
234
|
+
quote = null;
|
|
235
|
+
else
|
|
236
|
+
cur += c;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (quote === '"') {
|
|
240
|
+
if (c === '"')
|
|
241
|
+
quote = null;
|
|
242
|
+
else if (c === '\\' && (cmd[i + 1] === '"' || cmd[i + 1] === '\\'))
|
|
243
|
+
cur += cmd[++i];
|
|
244
|
+
else if (c === '$' || c === '`')
|
|
245
|
+
return null; // substitution inside "…"
|
|
246
|
+
else
|
|
247
|
+
cur += c;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (c === "'" || c === '"') {
|
|
251
|
+
quote = c;
|
|
252
|
+
has = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (c === ' ' || c === '\t') {
|
|
256
|
+
if (has) {
|
|
257
|
+
tokens.push(cur);
|
|
258
|
+
cur = '';
|
|
259
|
+
has = false;
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if ('|&;<>`$()\\\n\r*?{}[]~#!'.includes(c))
|
|
264
|
+
return null;
|
|
265
|
+
cur += c;
|
|
266
|
+
has = true;
|
|
267
|
+
}
|
|
268
|
+
if (quote)
|
|
269
|
+
return null; // unterminated quote
|
|
270
|
+
if (has)
|
|
271
|
+
tokens.push(cur);
|
|
272
|
+
return tokens;
|
|
273
|
+
}
|
package/lib/commands/test.js
CHANGED
|
@@ -126,6 +126,11 @@ export const testCommand = {
|
|
|
126
126
|
if (anyFailed) {
|
|
127
127
|
process.exit(1);
|
|
128
128
|
}
|
|
129
|
+
// Explicit success exit: under GJS the spawn-armed main loop would
|
|
130
|
+
// otherwise park this process after the summary. (runGjsBundle
|
|
131
|
+
// deliberately does NOT exit mid-flow — an unconditional exit there
|
|
132
|
+
// truncated this multi-runtime loop after the first gjs bundle.)
|
|
133
|
+
process.exit(0);
|
|
129
134
|
},
|
|
130
135
|
};
|
|
131
136
|
/** Build a single test bundle in-process via `BuildAction`. */
|
package/lib/commands/tsc.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// Reference: packages/infra/cli/src/commands/run.ts (env-setup precedent).
|
|
20
20
|
import { spawn } from 'node:child_process';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
22
23
|
import { pathToFileURL } from 'node:url';
|
|
23
24
|
import { detectNativePackages, buildNativeEnv } from '../utils/detect-native-packages.js';
|
|
24
25
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
@@ -63,16 +64,46 @@ export const tscCommand = {
|
|
|
63
64
|
// (e.g. `packages/web/fetch`) still finds the hoisted `@gjsify/tsc`
|
|
64
65
|
// at the root's `node_modules`. Falls back to cwd for standalone
|
|
65
66
|
// (non-monorepo) projects that have the package locally.
|
|
67
|
+
// Resolve `@gjsify/tsc/bundle` from two anchors, in order:
|
|
68
|
+
// 1. the consumer's workspace root / cwd (the normal install — a
|
|
69
|
+
// project that has `@gjsify/tsc` as a dev dependency), then
|
|
70
|
+
// 2. the running CLI bundle's own location (`import.meta.url`).
|
|
71
|
+
// Anchor 2 is what makes a *bundled* gjsify (the Flatpak SDK
|
|
72
|
+
// extension's `node_modules/@gjsify/tsc`, or any install where the CLI
|
|
73
|
+
// ships its own toolchain) resolve `@gjsify/tsc` even when the consumer
|
|
74
|
+
// project has none — mirroring how `bundler-pick.ts` dual-anchors the
|
|
75
|
+
// native bridges at cwd AND the bundle.
|
|
66
76
|
const anchorDir = findWorkspaceRoot(cwd) ?? cwd;
|
|
67
|
-
const
|
|
68
|
-
const require = createRequire(anchor);
|
|
77
|
+
const anchors = [pathToFileURL(`${anchorDir}/__gjsify_tsc__.js`).href, import.meta.url];
|
|
69
78
|
let bundlePath;
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
for (const anchor of anchors) {
|
|
80
|
+
try {
|
|
81
|
+
bundlePath = createRequire(anchor).resolve('@gjsify/tsc/bundle');
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// try the next anchor
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
// Node fallback: upstream npm `typescript`'s CLI (`lib/tsc.js`),
|
|
89
|
+
// resolved from the same anchors. gjsify tsc is GJS-first — the
|
|
90
|
+
// @gjsify/tsc bundle is the Node-free path — but it stays runnable
|
|
91
|
+
// under Node: on a machine with no `gjs`, it transparently spawns
|
|
92
|
+
// upstream `typescript`. Mirrors how `bundler-pick.ts` falls back to
|
|
93
|
+
// npm `rolldown` off GJS, so "both with both" holds for the type-gate.
|
|
94
|
+
let nodeTscPath;
|
|
95
|
+
for (const anchor of anchors) {
|
|
96
|
+
try {
|
|
97
|
+
nodeTscPath = join(dirname(createRequire(anchor).resolve('typescript/package.json')), 'lib', 'tsc.js');
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// try the next anchor
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!bundlePath && !nodeTscPath) {
|
|
105
|
+
console.error('gjsify tsc: neither @gjsify/tsc (GJS bundle) nor npm `typescript` (Node) is installed.');
|
|
106
|
+
console.error(' Install with: gjsify install --save-dev @gjsify/tsc (or add `typescript`).');
|
|
76
107
|
process.exit(1);
|
|
77
108
|
}
|
|
78
109
|
// Mirror `gjsify run`'s native-env composition so any future
|
|
@@ -85,25 +116,48 @@ export const tscCommand = {
|
|
|
85
116
|
...process.env,
|
|
86
117
|
...nativeEnv,
|
|
87
118
|
};
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
child.on('close', (code) =>
|
|
92
|
-
process.exit(code ?? 1);
|
|
93
|
-
});
|
|
119
|
+
// Run upstream `typescript` under this Node — the Node-only fallback.
|
|
120
|
+
const runNodeTsc = () => {
|
|
121
|
+
const child = spawn(process.execPath, [nodeTscPath, ...tscArgs], { env, stdio: 'inherit' });
|
|
122
|
+
child.on('close', (code) => process.exit(code ?? 1));
|
|
94
123
|
child.on('error', (err) => {
|
|
95
|
-
|
|
96
|
-
console.error('gjsify tsc: `gjs` not found on PATH. Install GJS (e.g. `dnf install gjs`).');
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
console.error(`gjsify tsc: ${err.message}`);
|
|
100
|
-
}
|
|
124
|
+
console.error(`gjsify tsc (node typescript): ${err.message}`);
|
|
101
125
|
process.exit(1);
|
|
102
126
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
};
|
|
128
|
+
// Prefer GJS — the Node-free @gjsify/tsc bundle — when it resolves. If
|
|
129
|
+
// `gjs` is not on PATH (ENOENT), transparently fall back to upstream
|
|
130
|
+
// `typescript` under Node when available.
|
|
131
|
+
if (bundlePath) {
|
|
132
|
+
const child = spawn('gjs', ['-m', bundlePath, ...tscArgs], { env, stdio: 'inherit' });
|
|
133
|
+
await new Promise((resolvePromise) => {
|
|
134
|
+
child.on('close', (code) => {
|
|
135
|
+
process.exit(code ?? 1);
|
|
136
|
+
});
|
|
137
|
+
child.on('error', (err) => {
|
|
138
|
+
if (err.code === 'ENOENT' && nodeTscPath) {
|
|
139
|
+
// No gjs on PATH — run upstream typescript under Node.
|
|
140
|
+
runNodeTsc();
|
|
141
|
+
}
|
|
142
|
+
else if (err.code === 'ENOENT') {
|
|
143
|
+
console.error('gjsify tsc: `gjs` not found on PATH and no npm `typescript` fallback.');
|
|
144
|
+
console.error(' Install GJS (e.g. `dnf install gjs`), or add `typescript` to the project.');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.error(`gjsify tsc: ${err.message}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// Never resolves naturally — process.exit() above terminates the
|
|
153
|
+
// program once the child closes. The promise just keeps the
|
|
154
|
+
// handler alive while the child runs.
|
|
155
|
+
void resolvePromise;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// No GJS bundle resolvable — go straight to the Node fallback.
|
|
160
|
+
runNodeTsc();
|
|
161
|
+
}
|
|
108
162
|
},
|
|
109
163
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,98 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// gjsify CLI entry point. The whole command surface lives in `cli-app.ts`
|
|
3
|
+
// (`runCli`), factored out so that `gjsify run` can dispatch a `gjsify
|
|
4
|
+
// <subcommand>` script IN-PROCESS instead of spawning a fresh gjs — the
|
|
5
|
+
// nested-gjs explosion that makes the GJS-first build orchestration thrash CI.
|
|
6
|
+
// This file is just the executable wrapper around `runCli`.
|
|
6
7
|
import { hideBin } from 'yargs/helpers';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
// Detect which runtime is executing the CLI (GJS or Node.js).
|
|
10
|
-
// GJS MUST be checked first because @gjsify/process sets
|
|
11
|
-
// `process.versions.node = '20.0.0'` for compatibility — a plain
|
|
12
|
-
// `process.versions.node` check would be a false Node positive under GJS.
|
|
13
|
-
function runtimeLabel() {
|
|
14
|
-
try {
|
|
15
|
-
const sys = globalThis.imports?.system;
|
|
16
|
-
if (sys?.version !== undefined) {
|
|
17
|
-
const v = Number(sys.version);
|
|
18
|
-
return `GJS ${Math.floor(v / 10000)}.${Math.floor((v % 10000) / 100)}.${v % 100} (SpiderMonkey)`;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
/* not GJS */
|
|
23
|
-
}
|
|
24
|
-
if (typeof process !== 'undefined' && typeof process.versions?.node === 'string') {
|
|
25
|
-
return `Node.js ${process.version}`;
|
|
26
|
-
}
|
|
27
|
-
return 'unknown runtime';
|
|
28
|
-
}
|
|
29
|
-
// Read the version from package.json adjacent to the bundle. yargs's
|
|
30
|
-
// auto-version-discovery (its `pkg-up`-driven default) doesn't reach
|
|
31
|
-
// through the bundled `dist/cli.gjs.mjs` path on GJS — falls back to
|
|
32
|
-
// "unknown". Both layouts are covered:
|
|
33
|
-
// - dev (tsx, `yarn workspace`): src/index.ts → ../package.json
|
|
34
|
-
// - bundled (install -g): dist/cli.gjs.mjs → ../package.json
|
|
35
|
-
function readBundleVersion() {
|
|
36
|
-
try {
|
|
37
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
39
|
-
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return 'unknown';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// `parseAsync()` instead of `.argv` so the top-level await keeps the
|
|
46
|
-
// process alive until command handlers complete. Under Node this is
|
|
47
|
-
// cosmetic — the event loop holds the process up — but under GJS the
|
|
48
|
-
// script ends as soon as the top-level synchronous flow finishes, and
|
|
49
|
-
// fire-and-forget handlers silently exit before any async work runs.
|
|
50
|
-
const cli = yargs(hideBin(process.argv));
|
|
51
|
-
await cli
|
|
52
|
-
.scriptName(APP_NAME)
|
|
53
|
-
.version(readBundleVersion())
|
|
54
|
-
.strict()
|
|
55
|
-
// Use the full terminal width for help. yargs's default caps at 80
|
|
56
|
-
// (`Math.min(80, process.stdout.columns)`); we explicitly opt into
|
|
57
|
-
// the real terminal width so long option/description lines wrap at
|
|
58
|
-
// the actual terminal edge instead of an arbitrary 80-col limit.
|
|
59
|
-
// `terminalWidth()` reads `process.stdout.columns`, which under GJS
|
|
60
|
-
// is backed by @gjsify/terminal-native (ioctl TIOCGWINSZ) when the
|
|
61
|
-
// typelib is on GI_TYPELIB_PATH — see the global launcher in
|
|
62
|
-
// packages/infra/cli/src/utils/install-global.ts.
|
|
63
|
-
.wrap(cli.terminalWidth())
|
|
64
|
-
.command(create.command, create.description, create.builder, create.handler)
|
|
65
|
-
.command(install.command, install.description, install.builder, install.handler)
|
|
66
|
-
.command(build.command, build.description, build.builder, build.handler)
|
|
67
|
-
.command(test.command, test.description, test.builder, test.handler)
|
|
68
|
-
.command(run.command, run.description, run.builder, run.handler)
|
|
69
|
-
.command(dlx.command, dlx.description, dlx.builder, dlx.handler)
|
|
70
|
-
.command(info.command, info.description, info.builder, info.handler)
|
|
71
|
-
.command(systemCheck.command, systemCheck.description, systemCheck.builder, systemCheck.handler)
|
|
72
|
-
.command(check.command, check.description, check.builder, check.handler)
|
|
73
|
-
.command(showcase.command, showcase.description, showcase.builder, showcase.handler)
|
|
74
|
-
.command(gresource.command, gresource.description, gresource.builder, gresource.handler)
|
|
75
|
-
.command(gettext.command, gettext.description, gettext.builder, gettext.handler)
|
|
76
|
-
.command(gsettings.command, gsettings.description, gsettings.builder, gsettings.handler)
|
|
77
|
-
.command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
|
|
78
|
-
.command(foreach.command, foreach.description, foreach.builder, foreach.handler)
|
|
79
|
-
.command(workspace.command, workspace.description, workspace.builder, workspace.handler)
|
|
80
|
-
.command(pack.command, pack.description, pack.builder, pack.handler)
|
|
81
|
-
.command(publish.command, publish.description, publish.builder, publish.handler)
|
|
82
|
-
.command(whoami.command, whoami.description, whoami.builder, whoami.handler)
|
|
83
|
-
.command(login.command, login.description, login.builder, login.handler)
|
|
84
|
-
.command(logout.command, logout.description, logout.builder, logout.handler)
|
|
85
|
-
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
86
|
-
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
87
|
-
.command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
|
|
88
|
-
.command(upgrade.command, upgrade.description, upgrade.builder, upgrade.handler)
|
|
89
|
-
.command(format.command, format.description, format.builder, format.handler)
|
|
90
|
-
.command(lint.command, lint.description, lint.builder, lint.handler)
|
|
91
|
-
.command(fix.command, fix.description, fix.builder, fix.handler)
|
|
92
|
-
.command(barrels.command, barrels.description, barrels.builder, barrels.handler)
|
|
93
|
-
.command(tsc.command, tsc.description, tsc.builder, tsc.handler)
|
|
94
|
-
.command(affected.command, affected.description, affected.builder, affected.handler)
|
|
95
|
-
.demandCommand(1)
|
|
96
|
-
.epilogue(`Running on ${runtimeLabel()}`)
|
|
97
|
-
.help()
|
|
98
|
-
.parseAsync();
|
|
8
|
+
import { runCli } from './cli-app.js';
|
|
9
|
+
await runCli(hideBin(process.argv));
|
package/lib/utils/npm-oidc.d.ts
CHANGED
|
@@ -20,8 +20,30 @@ export declare class OidcExchangeError extends Error {
|
|
|
20
20
|
readonly status: number;
|
|
21
21
|
readonly body: string;
|
|
22
22
|
readonly packageName: string;
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The non-secret claims decoded from the GitHub OIDC JWT that npm
|
|
25
|
+
* rejected — `repository`, `workflow_ref`, `environment`, … These are
|
|
26
|
+
* exactly what npm matches a Trusted Publisher config against, so on a
|
|
27
|
+
* 401/403 they tell you precisely which fields the config must carry.
|
|
28
|
+
*/
|
|
29
|
+
readonly claims?: Record<string, unknown> | undefined;
|
|
30
|
+
constructor(message: string, status: number, body: string, packageName: string,
|
|
31
|
+
/**
|
|
32
|
+
* The non-secret claims decoded from the GitHub OIDC JWT that npm
|
|
33
|
+
* rejected — `repository`, `workflow_ref`, `environment`, … These are
|
|
34
|
+
* exactly what npm matches a Trusted Publisher config against, so on a
|
|
35
|
+
* 401/403 they tell you precisely which fields the config must carry.
|
|
36
|
+
*/
|
|
37
|
+
claims?: Record<string, unknown> | undefined);
|
|
24
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Decode the (non-secret) payload of a JWT — the middle base64url segment.
|
|
41
|
+
* A GitHub OIDC id-token's payload is public metadata about the workflow run
|
|
42
|
+
* (`repository`, `repository_owner`, `workflow_ref`, `job_workflow_ref`,
|
|
43
|
+
* `ref`, `environment`, `sub`, …); the SIGNATURE is what makes it trustworthy.
|
|
44
|
+
* Returns null if the token isn't a well-formed three-part JWT.
|
|
45
|
+
*/
|
|
46
|
+
export declare function decodeJwtPayload(jwt: string): Record<string, unknown> | null;
|
|
25
47
|
/**
|
|
26
48
|
* Probe whether OIDC publishing is available in the current process —
|
|
27
49
|
* cheap env-var check, no network access. Used by `gjsify publish` to
|
package/lib/utils/npm-oidc.js
CHANGED
|
@@ -31,14 +31,43 @@ export class OidcExchangeError extends Error {
|
|
|
31
31
|
status;
|
|
32
32
|
body;
|
|
33
33
|
packageName;
|
|
34
|
-
|
|
34
|
+
claims;
|
|
35
|
+
constructor(message, status, body, packageName,
|
|
36
|
+
/**
|
|
37
|
+
* The non-secret claims decoded from the GitHub OIDC JWT that npm
|
|
38
|
+
* rejected — `repository`, `workflow_ref`, `environment`, … These are
|
|
39
|
+
* exactly what npm matches a Trusted Publisher config against, so on a
|
|
40
|
+
* 401/403 they tell you precisely which fields the config must carry.
|
|
41
|
+
*/
|
|
42
|
+
claims) {
|
|
35
43
|
super(message);
|
|
36
44
|
this.status = status;
|
|
37
45
|
this.body = body;
|
|
38
46
|
this.packageName = packageName;
|
|
47
|
+
this.claims = claims;
|
|
39
48
|
this.name = 'OidcExchangeError';
|
|
40
49
|
}
|
|
41
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Decode the (non-secret) payload of a JWT — the middle base64url segment.
|
|
53
|
+
* A GitHub OIDC id-token's payload is public metadata about the workflow run
|
|
54
|
+
* (`repository`, `repository_owner`, `workflow_ref`, `job_workflow_ref`,
|
|
55
|
+
* `ref`, `environment`, `sub`, …); the SIGNATURE is what makes it trustworthy.
|
|
56
|
+
* Returns null if the token isn't a well-formed three-part JWT.
|
|
57
|
+
*/
|
|
58
|
+
export function decodeJwtPayload(jwt) {
|
|
59
|
+
const parts = jwt.split('.');
|
|
60
|
+
if (parts.length !== 3)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
64
|
+
const json = typeof atob === 'function' ? atob(b64) : Buffer.from(b64, 'base64').toString('binary');
|
|
65
|
+
return JSON.parse(json);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
42
71
|
/**
|
|
43
72
|
* Probe whether OIDC publishing is available in the current process —
|
|
44
73
|
* cheap env-var check, no network access. Used by `gjsify publish` to
|
|
@@ -59,10 +88,19 @@ export async function fetchGithubOidcToken(audience, log) {
|
|
|
59
88
|
throw new OidcUnavailableError('GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_{URL,TOKEN}) not set. ' +
|
|
60
89
|
'The calling workflow needs `permissions: id-token: write`.', 'no-env');
|
|
61
90
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
// Append the audience by STRING CONCATENATION, mirroring @actions/core's
|
|
92
|
+
// getIDToken (`${runtimeUrl}&audience=${encodeURIComponent(aud)}`) and the
|
|
93
|
+
// raw-curl probe that returns HTTP 201. Do NOT use `URL.searchParams.set` +
|
|
94
|
+
// `.href`: under GJS that serialization dropped the `audience` query param,
|
|
95
|
+
// so GitHub issued an id-token with the DEFAULT audience (the repo API URL)
|
|
96
|
+
// instead of `npm:registry.npmjs.org`. npm then rejected the otherwise-valid
|
|
97
|
+
// token with 401 "unauthorized" — the JWT's repository/workflow_ref/sub
|
|
98
|
+
// claims all matched the Trusted Publisher, only `aud` was wrong, which is
|
|
99
|
+
// exactly the failure we saw (curl from the same workflow → 201).
|
|
100
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
101
|
+
const requestUrl = `${url}${sep}audience=${encodeURIComponent(audience)}`;
|
|
102
|
+
log?.(`gjsify oidc: GET ${requestUrl.replace(bearer, '<bearer>')}`);
|
|
103
|
+
const res = await fetch(requestUrl, {
|
|
66
104
|
method: 'GET',
|
|
67
105
|
headers: {
|
|
68
106
|
Accept: 'application/json',
|
|
@@ -114,7 +152,7 @@ export async function exchangeOidcForNpmToken(args) {
|
|
|
114
152
|
});
|
|
115
153
|
const text = await res.text().catch(() => '');
|
|
116
154
|
if (!res.ok) {
|
|
117
|
-
throw new OidcExchangeError(`npm OIDC token exchange failed for ${packageName}: ${res.status} ${res.statusText} — ${text.slice(0, 300)}`, res.status, text, packageName);
|
|
155
|
+
throw new OidcExchangeError(`npm OIDC token exchange failed for ${packageName}: ${res.status} ${res.statusText} — ${text.slice(0, 300)}`, res.status, text, packageName, decodeJwtPayload(idToken) ?? undefined);
|
|
118
156
|
}
|
|
119
157
|
let json;
|
|
120
158
|
try {
|
|
@@ -26,12 +26,27 @@ export interface RunOxcOptions {
|
|
|
26
26
|
verbose?: boolean;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Engine pick for oxfmt — mirrors `bundler-pick.ts`'s `shouldUseNative()`:
|
|
30
|
+
* - `GJSIFY_OXFMT=npm` forces the Node launcher.
|
|
31
|
+
* - `GJSIFY_OXFMT=native` forces the GI bridge and throws when it is not
|
|
32
|
+
* loadable (instead of silently switching engines).
|
|
33
|
+
* - Default: native under GJS when loadable (the Node launcher cannot run
|
|
34
|
+
* without a Node host), npm launcher on Node.
|
|
35
|
+
*/
|
|
36
|
+
export declare function shouldUseNativeOxfmt(): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Run an oxc tool (oxlint / oxfmt).
|
|
39
|
+
*
|
|
40
|
+
* oxfmt: dual-engine — under GJS the `@gjsify/oxfmt-native` GI bridge runs
|
|
41
|
+
* the full oxfmt CLI in-process (Node-free); otherwise the npm Node ESM
|
|
42
|
+
* launcher is spawned. `GJSIFY_OXFMT=native|npm` overrides the pick.
|
|
43
|
+
*
|
|
44
|
+
* oxlint: always spawned via its Node ESM launcher with the current Node
|
|
45
|
+
* executable (`process.execPath`) so the JS plugin host is available —
|
|
46
|
+
* required for the `jsPlugins`-wired `gjsify/register-class-order` rule.
|
|
32
47
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
48
|
+
* Inherits stdio so the tool's own output (diagnostics, reformatted files,
|
|
49
|
+
* summary lines) reaches the user.
|
|
35
50
|
*
|
|
36
51
|
* Returns the exit code; never throws on non-zero exit (callers check it).
|
|
37
52
|
*/
|
|
@@ -52,6 +67,22 @@ export declare function loadOxlintTemplate(): string;
|
|
|
52
67
|
export declare function loadOxfmtTemplate(): string;
|
|
53
68
|
/** Helper for callers to surface the install hint to the user cleanly. */
|
|
54
69
|
export declare function printOxcNotFound(err: OxcNotFoundError): void;
|
|
70
|
+
/**
|
|
71
|
+
* Propagate an oxc tool's exit code from a command handler.
|
|
72
|
+
*
|
|
73
|
+
* On Node, setting `process.exitCode` is the gentle idiom (streams flush,
|
|
74
|
+
* the process ends naturally). Under GJS there is no atexit hook, so
|
|
75
|
+
* `process.exitCode` alone is NOT honored at natural shutdown — the gjs
|
|
76
|
+
* process would exit 0 after a failed `--check`. Call `imports.system.exit`
|
|
77
|
+
* directly for non-zero codes there: SpiderMonkey raises its uncatchable
|
|
78
|
+
* exit exception from any context — including the yargs `parseAsync`
|
|
79
|
+
* microtask continuation this runs in — and stdout writes are synchronous
|
|
80
|
+
* under GJS, so the tool's report is already flushed. (`process.exit`'s
|
|
81
|
+
* idle-scheduled path is NOT used here: the scheduled idle never fires from
|
|
82
|
+
* this continuation — the module's top-level await is still pending and gjs
|
|
83
|
+
* exits 0 before the hook-registered loop runs.)
|
|
84
|
+
*/
|
|
85
|
+
export declare function setOxcExitCode(code: number): void;
|
|
55
86
|
/**
|
|
56
87
|
* Has the given oxc tool's npm package (or its companion) been declared in the
|
|
57
88
|
* project's dependencies? Useful as a cheap pre-flight check — `gjsify flatpak
|