@gjsify/cli 0.29.0 → 0.31.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 +197 -189
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +78 -3
- package/lib/actions/index.d.ts +2 -0
- package/lib/actions/index.js +2 -0
- package/lib/affected-entry.d.ts +1 -0
- package/lib/affected-entry.js +49 -0
- package/lib/bundler-pick.js +84 -19
- package/lib/cli-app.js +20 -0
- package/lib/cli-fail.d.ts +22 -0
- package/lib/cli-fail.js +67 -0
- package/lib/commands/affected.d.ts +2 -2
- package/lib/commands/affected.js +16 -0
- package/lib/commands/install.d.ts +55 -0
- package/lib/commands/install.js +38 -21
- package/lib/commands/run.js +25 -6
- package/lib/commands/test.js +4 -4
- package/lib/commands/tsc.js +59 -11
- package/lib/commands/workspace.js +13 -0
- package/lib/config.js +6 -2
- package/lib/index.js +18 -1
- package/lib/templates/install.mjs.tmpl +27 -2
- package/lib/types/command.d.ts +18 -0
- package/lib/utils/bin-shim.d.ts +39 -0
- package/lib/utils/bin-shim.js +89 -13
- package/lib/utils/check-system-deps.d.ts +88 -1
- package/lib/utils/check-system-deps.js +177 -6
- package/lib/utils/detect-native-packages.js +12 -1
- package/lib/utils/discover-showcases.d.ts +0 -2
- package/lib/utils/discover-showcases.js +0 -1
- package/lib/utils/dlx-cache.d.ts +22 -2
- package/lib/utils/dlx-cache.js +62 -6
- package/lib/utils/force-exit.d.ts +10 -0
- package/lib/utils/force-exit.js +43 -0
- package/lib/utils/gi-search-path.d.ts +36 -0
- package/lib/utils/gi-search-path.js +146 -0
- package/lib/utils/gjs-source-escape.d.ts +10 -0
- package/lib/utils/gjs-source-escape.js +49 -0
- package/lib/utils/gjsify-shim.d.ts +29 -2
- package/lib/utils/gjsify-shim.js +131 -22
- package/lib/utils/install-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +8 -2
- package/lib/utils/install-global.d.ts +30 -2
- package/lib/utils/install-global.js +51 -5
- package/lib/utils/oxc-resolve.d.ts +1 -1
- package/lib/utils/oxc-resolve.js +10 -6
- package/lib/utils/run-lifecycle-script.js +41 -25
- package/lib/utils/spawn.d.ts +17 -1
- package/lib/utils/spawn.js +23 -6
- package/lib/utils/stdin.d.ts +3 -13
- package/lib/utils/stdin.js +21 -100
- package/lib/utils/system-gi.js +10 -1
- package/package.json +22 -17
- package/showcases.json +8 -16
|
@@ -1766,7 +1766,7 @@ function readShebang(file) {
|
|
|
1766
1766
|
* the same set from the same templates. See `./bin-shim.ts`.
|
|
1767
1767
|
*/
|
|
1768
1768
|
export function writeBinEntry(opts) {
|
|
1769
|
-
const { binDir, binName, targetAbs, gjs } = opts;
|
|
1769
|
+
const { binDir, binName, targetAbs, gjs, nodeFallbackAbs } = opts;
|
|
1770
1770
|
const platform = opts.platform ?? process.platform;
|
|
1771
1771
|
const linkPath = path.join(binDir, binName);
|
|
1772
1772
|
if (gjs) {
|
|
@@ -1775,7 +1775,7 @@ export function writeBinEntry(opts) {
|
|
|
1775
1775
|
// plain symlink (or npm's three-file shim) under this very name.
|
|
1776
1776
|
for (const stale of [linkPath, `${linkPath}.cmd`, `${linkPath}.ps1`])
|
|
1777
1777
|
rmWithRetry(stale);
|
|
1778
|
-
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble: gjs.envPreamble, isGjsBundle: isBundle }));
|
|
1778
|
+
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble: gjs.envPreamble, isGjsBundle: isBundle, nodeFallbackAbs }));
|
|
1779
1779
|
try {
|
|
1780
1780
|
fs.chmodSync(linkPath, 0o755);
|
|
1781
1781
|
}
|
|
@@ -1793,6 +1793,7 @@ export function writeBinEntry(opts) {
|
|
|
1793
1793
|
interpreterArgs: ['-m'],
|
|
1794
1794
|
target: targetAbs,
|
|
1795
1795
|
prependEnv,
|
|
1796
|
+
fallback: nodeFallbackAbs === undefined ? undefined : { interpreter: 'node', target: nodeFallbackAbs },
|
|
1796
1797
|
});
|
|
1797
1798
|
fs.writeFileSync(`${linkPath}.cmd`, shims.cmd);
|
|
1798
1799
|
fs.writeFileSync(`${linkPath}.ps1`, shims.ps1);
|
|
@@ -1872,11 +1873,16 @@ async function linkBins(nodes, prefix, log) {
|
|
|
1872
1873
|
catch {
|
|
1873
1874
|
/* best effort */
|
|
1874
1875
|
}
|
|
1876
|
+
// Only a fallback that EXISTS is worth writing: a launcher naming a
|
|
1877
|
+
// missing Node entry would trade exit 127 for a different exit 127.
|
|
1878
|
+
const fallbackRel = picked.nodeFallback?.get(binName);
|
|
1879
|
+
const fallbackAbs = fallbackRel === undefined ? undefined : path.join(pkgDir, fallbackRel);
|
|
1875
1880
|
writeBinEntry({
|
|
1876
1881
|
binDir,
|
|
1877
1882
|
binName,
|
|
1878
1883
|
targetAbs,
|
|
1879
1884
|
gjs: picked.isGjsBin ? gjsEnvForPrefix() : undefined,
|
|
1885
|
+
nodeFallbackAbs: fallbackAbs !== undefined && fs.existsSync(fallbackAbs) ? fallbackAbs : undefined,
|
|
1880
1886
|
});
|
|
1881
1887
|
created++;
|
|
1882
1888
|
}
|
|
@@ -29,8 +29,36 @@ export declare function defaultGlobalLayout(): GlobalLayout;
|
|
|
29
29
|
* story already degrades on Node.
|
|
30
30
|
*/
|
|
31
31
|
export declare const GJS_ENGINE_PACKAGES: readonly ["@gjsify/rolldown-native", "@gjsify/lightningcss-native", "@gjsify/oxfmt-native"];
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The one engine whose absence is fatal rather than degrading: `gjsify build`
|
|
34
|
+
* has no fallback under GJS. Named separately because two different questions
|
|
35
|
+
* ("what do we install") and ("is a build possible") want different sets.
|
|
36
|
+
*/
|
|
37
|
+
export declare const BUNDLER_ENGINE_PACKAGE = "@gjsify/rolldown-native";
|
|
38
|
+
/**
|
|
39
|
+
* True when a bundler engine `gjsify build` can actually LOAD is reachable from
|
|
40
|
+
* `dir`.
|
|
41
|
+
*
|
|
42
|
+
* This used to `existsSync` the bridge's own `package.json` and stop there. That
|
|
43
|
+
* answer was correct before ADR 0017 and is wrong after it: the bridge is now
|
|
44
|
+
* pure TypeScript (`files: ["lib", "meson.build", "src/rust", "src/vala"]`) and
|
|
45
|
+
* the binary lives in a per-target child (`@gjsify/rolldown-native-<os>-<arch>`).
|
|
46
|
+
* So "bridge present, child missing" reported `true` — which is why
|
|
47
|
+
* `self-update`'s repair gate could say "Already up to date" about a prefix whose
|
|
48
|
+
* engine cannot load. A live defect, not a hypothetical one.
|
|
49
|
+
*
|
|
50
|
+
* Asks `detectNativePackages` instead, which is the same walk-up
|
|
51
|
+
* `findRolldownNativeDir` performs at BUILD time. That matters more than the
|
|
52
|
+
* shortcut it replaces: an installer that disagrees with the builder about
|
|
53
|
+
* whether the engine is present is worse than one that is merely slow. It also
|
|
54
|
+
* makes the gjsify monorepo a no-op — the workspace symlink resolves — which is
|
|
55
|
+
* what keeps ADR 0001's non-destructive invariant safe from a registry copy
|
|
56
|
+
* landing on top of `packages/infra/rolldown-native`.
|
|
57
|
+
*
|
|
58
|
+
* `dir` is a prefix for a global install and a project root for a project one;
|
|
59
|
+
* the walk handles both.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasBundlerEngineInstalled(dir: string): boolean;
|
|
34
62
|
/**
|
|
35
63
|
* Best-effort install of the GJS-native engine packages into a global prefix.
|
|
36
64
|
*
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import * as fs from 'node:fs';
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
|
-
import { detectNativePackages } from './detect-native-packages.js';
|
|
26
|
+
import { detectHostLibc, detectNativePackages, hostPlatformTokens, platformPackageName, } from './detect-native-packages.js';
|
|
27
27
|
import { buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, isGjsBundlePath, parseShebang, pickBinMap, } from './bin-shim.js';
|
|
28
28
|
/**
|
|
29
29
|
* Compute the canonical global install layout for the current user. Honours
|
|
@@ -63,9 +63,43 @@ export const GJS_ENGINE_PACKAGES = [
|
|
|
63
63
|
'@gjsify/lightningcss-native',
|
|
64
64
|
'@gjsify/oxfmt-native',
|
|
65
65
|
];
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The one engine whose absence is fatal rather than degrading: `gjsify build`
|
|
68
|
+
* has no fallback under GJS. Named separately because two different questions
|
|
69
|
+
* ("what do we install") and ("is a build possible") want different sets.
|
|
70
|
+
*/
|
|
71
|
+
export const BUNDLER_ENGINE_PACKAGE = '@gjsify/rolldown-native';
|
|
72
|
+
/**
|
|
73
|
+
* True when a bundler engine `gjsify build` can actually LOAD is reachable from
|
|
74
|
+
* `dir`.
|
|
75
|
+
*
|
|
76
|
+
* This used to `existsSync` the bridge's own `package.json` and stop there. That
|
|
77
|
+
* answer was correct before ADR 0017 and is wrong after it: the bridge is now
|
|
78
|
+
* pure TypeScript (`files: ["lib", "meson.build", "src/rust", "src/vala"]`) and
|
|
79
|
+
* the binary lives in a per-target child (`@gjsify/rolldown-native-<os>-<arch>`).
|
|
80
|
+
* So "bridge present, child missing" reported `true` — which is why
|
|
81
|
+
* `self-update`'s repair gate could say "Already up to date" about a prefix whose
|
|
82
|
+
* engine cannot load. A live defect, not a hypothetical one.
|
|
83
|
+
*
|
|
84
|
+
* Asks `detectNativePackages` instead, which is the same walk-up
|
|
85
|
+
* `findRolldownNativeDir` performs at BUILD time. That matters more than the
|
|
86
|
+
* shortcut it replaces: an installer that disagrees with the builder about
|
|
87
|
+
* whether the engine is present is worse than one that is merely slow. It also
|
|
88
|
+
* makes the gjsify monorepo a no-op — the workspace symlink resolves — which is
|
|
89
|
+
* what keeps ADR 0001's non-destructive invariant safe from a registry copy
|
|
90
|
+
* landing on top of `packages/infra/rolldown-native`.
|
|
91
|
+
*
|
|
92
|
+
* `dir` is a prefix for a global install and a project root for a project one;
|
|
93
|
+
* the walk handles both.
|
|
94
|
+
*/
|
|
95
|
+
export function hasBundlerEngineInstalled(dir) {
|
|
96
|
+
const names = new Set(detectNativePackages(dir).map((p) => p.name));
|
|
97
|
+
if (names.has(BUNDLER_ENGINE_PACKAGE))
|
|
98
|
+
return true;
|
|
99
|
+
// The per-target child is the thing that actually carries the binary, so its
|
|
100
|
+
// presence is the honest positive answer even when the umbrella is absent.
|
|
101
|
+
const libc = detectHostLibc(process.platform);
|
|
102
|
+
return hostPlatformTokens(process.platform, process.arch, libc).some((token) => names.has(platformPackageName(BUNDLER_ENGINE_PACKAGE, token)));
|
|
69
103
|
}
|
|
70
104
|
/**
|
|
71
105
|
* Best-effort install of the GJS-native engine packages into a global prefix.
|
|
@@ -198,10 +232,19 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
198
232
|
// then tries to parse JavaScript as shell. Plain Node scripts
|
|
199
233
|
// with shebangs (lib/index.js) keep the direct-exec path.
|
|
200
234
|
const isGjsBundle = isGjsBundlePath(targetAbs);
|
|
235
|
+
// A global install extracts the WHOLE package, so when it declares
|
|
236
|
+
// an npm `bin` beside its `gjsify.bin` that entry is on disk here
|
|
237
|
+
// too and the launcher can fall back to it on a host without `gjs`
|
|
238
|
+
// — the same fix as the project-local backend. Guarded on existence
|
|
239
|
+
// rather than assumed: naming a missing Node entry would only trade
|
|
240
|
+
// one exit 127 for another. See `pickBinMap`.
|
|
241
|
+
const fallbackRel = picked.nodeFallback?.get(binName);
|
|
242
|
+
const fallbackCandidate = fallbackRel === undefined ? undefined : path.join(pkgDir, fallbackRel);
|
|
243
|
+
const nodeFallbackAbs = fallbackCandidate !== undefined && fs.existsSync(fallbackCandidate) ? fallbackCandidate : undefined;
|
|
201
244
|
// Only GJS bundles need the GI typelib search path. Plain Node
|
|
202
245
|
// scripts ignore GI_TYPELIB_PATH, so skipping the preamble there
|
|
203
246
|
// keeps the launcher minimal.
|
|
204
|
-
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble, isGjsBundle }));
|
|
247
|
+
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble, isGjsBundle, nodeFallbackAbs }));
|
|
205
248
|
fs.chmodSync(linkPath, 0o755);
|
|
206
249
|
// Windows runs neither an extension-less file nor a `#!` line, so
|
|
207
250
|
// the `sh` launcher above is only reachable from git-bash/MSYS/WSL.
|
|
@@ -224,6 +267,9 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
224
267
|
interpreterArgs: ['-m'],
|
|
225
268
|
target: targetAbs,
|
|
226
269
|
prependEnv,
|
|
270
|
+
fallback: nodeFallbackAbs === undefined
|
|
271
|
+
? undefined
|
|
272
|
+
: { interpreter: 'node', target: nodeFallbackAbs },
|
|
227
273
|
})
|
|
228
274
|
: buildLauncherShims({
|
|
229
275
|
interpreter: shebang?.prog || 'node',
|
|
@@ -69,7 +69,7 @@ export declare function shouldUseNativeOxfmt(): Promise<boolean>;
|
|
|
69
69
|
*
|
|
70
70
|
* oxlint: always spawned via its Node ESM launcher on a real Node host
|
|
71
71
|
* (`nodeBinary()`: `process.execPath` under Node, PATH `node` under the GJS
|
|
72
|
-
* bundle where `process.execPath` is the
|
|
72
|
+
* bundle where `process.execPath` is the `gjs` interpreter) so the JS plugin host
|
|
73
73
|
* is available — required for the `jsPlugins`-wired `gjsify/register-class-order`
|
|
74
74
|
* rule. Under GJS a `node` on PATH is required (oxlint has no native bridge
|
|
75
75
|
* yet); its absence surfaces as a clear OxcNotFoundError, never a hang.
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -53,6 +53,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
53
53
|
import { hostLibc } from './platform-check.js';
|
|
54
54
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
55
55
|
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
56
|
+
import { activateNativePrebuilds } from './gi-search-path.js';
|
|
56
57
|
import { nodeBinary } from './run-node.js';
|
|
57
58
|
import { spawnToCompletion } from './spawn.js';
|
|
58
59
|
import { isGjs, gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
@@ -245,6 +246,10 @@ async function tryLoadNativeOxfmt() {
|
|
|
245
246
|
if (!isGjs())
|
|
246
247
|
return null;
|
|
247
248
|
try {
|
|
249
|
+
// Same reason as `bundler-pick.ts`'s `tryLoadNative()`: put the
|
|
250
|
+
// prebuild on girepository's own search paths so the bridge loads
|
|
251
|
+
// without the `gjsify` launcher having exported anything. ADR 0021.
|
|
252
|
+
activateNativePrebuilds();
|
|
248
253
|
const specifier = '@gjsify/oxfmt-native';
|
|
249
254
|
const resolved = resolveNpmPackage(specifier, { bundleUrl: import.meta.url }) ??
|
|
250
255
|
createRequire(import.meta.url).resolve(specifier);
|
|
@@ -293,7 +298,7 @@ export async function shouldUseNativeOxfmt() {
|
|
|
293
298
|
*
|
|
294
299
|
* oxlint: always spawned via its Node ESM launcher on a real Node host
|
|
295
300
|
* (`nodeBinary()`: `process.execPath` under Node, PATH `node` under the GJS
|
|
296
|
-
* bundle where `process.execPath` is the
|
|
301
|
+
* bundle where `process.execPath` is the `gjs` interpreter) so the JS plugin host
|
|
297
302
|
* is available — required for the `jsPlugins`-wired `gjsify/register-class-order`
|
|
298
303
|
* rule. Under GJS a `node` on PATH is required (oxlint has no native bridge
|
|
299
304
|
* yet); its absence surfaces as a clear OxcNotFoundError, never a hang.
|
|
@@ -323,11 +328,10 @@ export async function runOxc(tool, args, opts = {}) {
|
|
|
323
328
|
/**
|
|
324
329
|
* Spawn an oxc tool via its Node ESM launcher (`node_modules/<tool>/bin/…`) on
|
|
325
330
|
* a real Node host. CRITICAL: use `nodeBinary()`, not `process.execPath`
|
|
326
|
-
* directly — under
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* resolves to PATH `node` under GJS, so the launcher runs on Node as required.
|
|
331
|
+
* directly — under GJS `process.execPath` is the `gjs` interpreter, so spawning
|
|
332
|
+
* it runs the Node ESM launcher under GJS, where the plugin host does not exist.
|
|
333
|
+
* `nodeBinary()` resolves to PATH `node` under GJS, so the launcher runs on Node
|
|
334
|
+
* as required.
|
|
331
335
|
*
|
|
332
336
|
* `completion: 'return'`: `lint` / `format` / `fix` hand the tool's exit code
|
|
333
337
|
* to `setOxcExitCode`, which deliberately does NOT terminate the process on a
|
|
@@ -7,8 +7,28 @@
|
|
|
7
7
|
// behaves like `yarn run` / `npm run`. That's wrong for the embedded use
|
|
8
8
|
// case where pack/publish needs the lifecycle script to finish, then keep
|
|
9
9
|
// running its own logic. This helper resolves to a Promise that settles
|
|
10
|
-
// when the child exits
|
|
11
|
-
//
|
|
10
|
+
// when the child exits.
|
|
11
|
+
//
|
|
12
|
+
// ## Not calling `process.exit()` is what ARMED the hang, not what avoided it
|
|
13
|
+
//
|
|
14
|
+
// This header used to end "no process.exit, no GLib-mainloop intermingling
|
|
15
|
+
// from `ensureMainLoop`". The second half was exactly backwards, and it is why
|
|
16
|
+
// nobody looked here for #1010.
|
|
17
|
+
//
|
|
18
|
+
// `@gjsify/child_process`'s async `spawn()` calls `ensureMainLoop()`
|
|
19
|
+
// unconditionally — the caller has no say. That arms a GJS main-loop hook whose
|
|
20
|
+
// blocking `g_main_loop_run()` only `process.exit()` tears down. So a helper
|
|
21
|
+
// that spawns asynchronously and deliberately does NOT exit leaves the loop
|
|
22
|
+
// armed forever: `gjsify pack` on a package with a `prepack` wrote its tarball,
|
|
23
|
+
// printed its complete JSON, and then parked at 0% CPU until killed at 5m30s.
|
|
24
|
+
// A package with no lifecycle script exited in under a second, and the same
|
|
25
|
+
// pack under Node exited in 0.68s.
|
|
26
|
+
//
|
|
27
|
+
// `utils/spawn.ts` already owns that contract and makes every call site DECLARE
|
|
28
|
+
// which side of it they are on. This helper was the one spawn in the CLI that
|
|
29
|
+
// bypassed it — the seventh of the "six near-identical wrappers" that file was
|
|
30
|
+
// written to consolidate. It now goes through it with `completion: 'return'`,
|
|
31
|
+
// which is what "settle, then keep running our own logic" means.
|
|
12
32
|
//
|
|
13
33
|
// Matches yarn / npm script semantics:
|
|
14
34
|
// - `shell: true` so `&&` / `|` / env-var refs work
|
|
@@ -16,8 +36,8 @@
|
|
|
16
36
|
// - `npm_lifecycle_event` / `npm_package_name` / `npm_package_version`
|
|
17
37
|
// env vars set
|
|
18
38
|
// - FORCE_COLOR=1 default unless caller overrides
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
39
|
import { delimiter, join } from 'node:path';
|
|
40
|
+
import { describeExit, spawnToCompletion } from './spawn.js';
|
|
21
41
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
22
42
|
/**
|
|
23
43
|
* Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
|
|
@@ -52,28 +72,24 @@ export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
|
|
|
52
72
|
npm_package_version: pkg.version ?? '',
|
|
53
73
|
...opts.env,
|
|
54
74
|
};
|
|
55
|
-
// `'
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (code === 0)
|
|
71
|
-
resolveOk();
|
|
72
|
-
else {
|
|
73
|
-
reject(new Error(`gjsify lifecycle-script: "${name}" in ${wsDir} exited with code ${code}`));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
child.on('error', reject);
|
|
75
|
+
// `'ignore'` has no `spawnToCompletion` equivalent and no call site in the
|
|
76
|
+
// tree; folding it onto `'inherit'` would silently start printing where a
|
|
77
|
+
// caller asked for silence, so it is rejected rather than approximated.
|
|
78
|
+
if (opts.stdio === 'ignore') {
|
|
79
|
+
throw new Error('gjsify lifecycle-script: stdio "ignore" is not supported — see utils/spawn.ts');
|
|
80
|
+
}
|
|
81
|
+
const result = await spawnToCompletion(literal, [], {
|
|
82
|
+
// The whole point of this helper: pack/publish waits for the script and
|
|
83
|
+
// then keeps running. Under GJS that is the blocking path, which is what
|
|
84
|
+
// keeps the armed main loop from outliving us — see the header.
|
|
85
|
+
completion: 'return',
|
|
86
|
+
cwd: wsDir,
|
|
87
|
+
env: env,
|
|
88
|
+
shell: true,
|
|
89
|
+
stdio: opts.stdio === 'inherit-stderr' ? 'inherit-stderr' : opts.stdio === 'pipe' ? 'pipe' : 'inherit',
|
|
77
90
|
});
|
|
91
|
+
if (result.code !== 0) {
|
|
92
|
+
throw new Error(`gjsify lifecycle-script: "${name}" in ${wsDir} exited with ${describeExit(result)}`);
|
|
93
|
+
}
|
|
78
94
|
return true;
|
|
79
95
|
}
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -33,8 +33,24 @@ export interface SpawnToCompletionOptions {
|
|
|
33
33
|
* `'inherit'` (default) forwards the parent's stdio. `'pipe'` gives the
|
|
34
34
|
* caller `child.stdout` / `child.stderr` via {@link onSpawn} — only
|
|
35
35
|
* meaningful together with `completion: 'exit'` (see {@link onSpawn}).
|
|
36
|
+
*
|
|
37
|
+
* `'inherit-stderr'` inherits, but routes the child's **stdout to the
|
|
38
|
+
* parent's stderr**. For a parent whose own stdout is a machine-readable
|
|
39
|
+
* stream: `gjsify pack --json` emits JSON there, and a lifecycle script's
|
|
40
|
+
* log lines interleaved into it make the result unparseable for every
|
|
41
|
+
* caller that pipes it into `JSON.parse`.
|
|
42
|
+
*/
|
|
43
|
+
stdio?: 'inherit' | 'pipe' | 'inherit-stderr';
|
|
44
|
+
/**
|
|
45
|
+
* Run `cmd` through the platform shell, so `&&`, `|` and env-var
|
|
46
|
+
* references work. `cmd` is then a whole command LINE and `args` must be
|
|
47
|
+
* empty — that is npm/yarn script semantics.
|
|
48
|
+
*
|
|
49
|
+
* Skips the win32 command rewrite by design, per this file's own rule:
|
|
50
|
+
* Node routes a `shell: true` spawn through `%COMSPEC%`, which finds
|
|
51
|
+
* `.cmd` shims on its own, so rewriting the string would corrupt it.
|
|
36
52
|
*/
|
|
37
|
-
|
|
53
|
+
shell?: boolean;
|
|
38
54
|
/**
|
|
39
55
|
* Invoked synchronously with the live `ChildProcess` right after a
|
|
40
56
|
* successful async spawn — the seam for kill-tracking registries and for
|
package/lib/utils/spawn.js
CHANGED
|
@@ -133,34 +133,51 @@ function mustBlock(opts) {
|
|
|
133
133
|
*/
|
|
134
134
|
export function spawnToCompletion(cmd, args, opts) {
|
|
135
135
|
const env = resolveEnv(opts);
|
|
136
|
+
// A shell invocation is a command LINE, not an argv, so the win32 rewrite
|
|
137
|
+
// must not touch it — see `SpawnToCompletionOptions.shell`.
|
|
138
|
+
const invocation = opts.shell
|
|
139
|
+
? { cmd, args: [...args], windowsVerbatimArguments: false }
|
|
140
|
+
: forWin32(cmd, args, env);
|
|
136
141
|
if (mustBlock(opts)) {
|
|
137
142
|
// `'inherit'` is NOT forwarded through `@gjsify/child_process`'s GJS
|
|
138
143
|
// spawnSync path (it drives a private main context via
|
|
139
144
|
// `communicateWithTimeout`), so the child's diagnostics would be
|
|
140
145
|
// silently dropped. Capture them and re-emit after the child exits.
|
|
141
146
|
// stdin stays inherited so an interactive child still reads the tty.
|
|
142
|
-
const
|
|
143
|
-
const r = spawnSync(win.cmd, win.args, {
|
|
147
|
+
const r = spawnSync(invocation.cmd, invocation.args, {
|
|
144
148
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
145
149
|
cwd: opts.cwd,
|
|
146
150
|
env,
|
|
151
|
+
shell: opts.shell,
|
|
147
152
|
maxBuffer: opts.maxBuffer ?? DEFAULT_MAX_BUFFER,
|
|
148
|
-
windowsVerbatimArguments:
|
|
153
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
149
154
|
});
|
|
150
155
|
if (r.error)
|
|
151
156
|
return Promise.reject(mapSpawnError(r.error, opts));
|
|
157
|
+
// The captured stdout goes to the parent's STDERR under
|
|
158
|
+
// `'inherit-stderr'` — same destination the streaming path routes it to,
|
|
159
|
+
// so the two agree on where a lifecycle script's chatter lands.
|
|
160
|
+
const out = opts.stdio === 'inherit-stderr' ? process.stderr : process.stdout;
|
|
152
161
|
if (r.stdout && r.stdout.length > 0)
|
|
153
|
-
|
|
162
|
+
out.write(r.stdout);
|
|
154
163
|
if (r.stderr && r.stderr.length > 0)
|
|
155
164
|
process.stderr.write(r.stderr);
|
|
156
165
|
return Promise.resolve({ code: r.status, signal: r.signal ?? null });
|
|
157
166
|
}
|
|
158
167
|
return new Promise((resolvePromise, reject) => {
|
|
159
|
-
const win =
|
|
168
|
+
const win = invocation;
|
|
160
169
|
const spawnOpts = {
|
|
161
170
|
cwd: opts.cwd,
|
|
162
|
-
|
|
171
|
+
// Numeric fds route the child's matching stream to that fd, so
|
|
172
|
+
// `['inherit', 2, 2]` is stdin inherited, stdout AND stderr → fd 2.
|
|
173
|
+
stdio: opts.stdio === 'pipe'
|
|
174
|
+
? ['ignore', 'pipe', 'pipe']
|
|
175
|
+
: opts.stdio === 'inherit-stderr'
|
|
176
|
+
? ['inherit', 2, 2]
|
|
177
|
+
: 'inherit',
|
|
163
178
|
};
|
|
179
|
+
if (opts.shell)
|
|
180
|
+
spawnOpts.shell = true;
|
|
164
181
|
if (env)
|
|
165
182
|
spawnOpts.env = env;
|
|
166
183
|
if (win.windowsVerbatimArguments)
|
package/lib/utils/stdin.d.ts
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read the whole of stdin as UTF-8. Works on Node and under the GJS bundle.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `@gjsify/fs` has no numeric-fd path, so the `0` is coerced to a relative
|
|
8
|
-
* PATH and the call opens `./0`. A GJS run that reaches that line is already
|
|
9
|
-
* lost, so using it as GJS's fallback only converts a clear failure into
|
|
10
|
-
* `ENOENT … read '0'`, which is what a capability probe returning a false
|
|
11
|
-
* negative did.
|
|
12
|
-
*
|
|
13
|
-
* So: the presence of `imports.gi` decides. On GJS both GJS readers are tried
|
|
14
|
-
* and a failure THROWS with what was attempted; the Node reader is never
|
|
15
|
-
* reached. Off GJS there is no gi namespace and `readFileSync(0)` is the only
|
|
16
|
-
* and correct answer.
|
|
4
|
+
* Synchronous on purpose: the callers are CLI argument-parsing paths that run
|
|
5
|
+
* before any main loop exists, and the async `process.stdin` route carries the
|
|
6
|
+
* documented resume/pause races (see `utils/prompt.ts`).
|
|
17
7
|
*/
|
|
18
8
|
export declare function readStdinText(): string;
|
|
19
9
|
/**
|
package/lib/utils/stdin.js
CHANGED
|
@@ -1,119 +1,40 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Read ALL of stdin, on Node AND inside the committed GJS bundles.
|
|
3
3
|
//
|
|
4
|
-
// `readFileSync(0, 'utf-8')` is the Node idiom for "slurp stdin"
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// caller died with
|
|
4
|
+
// `readFileSync(0, 'utf-8')` is the Node idiom for "slurp stdin". It used NOT to
|
|
5
|
+
// survive the GJS bundle: `@gjsify/fs` had no numeric-file-descriptor path, so
|
|
6
|
+
// the `0` was coerced to a PATH and the call opened the relative file `./0` —
|
|
7
|
+
// every GJS caller died with
|
|
9
8
|
//
|
|
10
9
|
// Error: ENOENT: … read '0' (readStdinLines → readFileSync)
|
|
11
10
|
//
|
|
12
11
|
// which silently disabled the ONE fixture-driven entry point AGENTS.md
|
|
13
12
|
// documents for testing the CI classifier (`--changed-from-stdin`).
|
|
14
13
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// THE GAP IS CLOSED AT THE CORE, so the workaround is gone: `@gjsify/fs` maps
|
|
15
|
+
// the parent-supplied descriptors 0/1/2 onto the process's own Unix streams
|
|
16
|
+
// (`packages/node/fs/src/std-fd.ts`, reached from `readFileSync` through
|
|
17
|
+
// `sync.ts`), which makes the Node idiom mean the same thing under GJS. This
|
|
18
|
+
// module used to carry two GJS-only readers for that gap — a
|
|
19
|
+
// `GioUnix.InputStream` chunk loop and a `/dev/stdin` `Gio.File` fallback —
|
|
20
|
+
// plus a runtime branch to choose between them. All three were consumer-side
|
|
21
|
+
// repair for a missing core capability.
|
|
17
22
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// The legacy `imports.gi` form is the sanctioned GJS-bootstrap escape
|
|
24
|
-
// hatch (AGENTS.md "Reading globals", exception 4) and adds NO new typelib
|
|
25
|
-
// to the classifier bundle — `gi://GioUnix?version=2.0` is already in its
|
|
26
|
-
// import graph via `@gjsify/fs`.
|
|
27
|
-
// - a synchronous read. The callers are CLI argument-parsing paths that run
|
|
28
|
-
// before any main loop exists, and the async `process.stdin` route carries
|
|
29
|
-
// the documented resume/pause races (see `utils/prompt.ts`).
|
|
23
|
+
// Anything re-adding a GJS branch HERE is re-adding the workaround. If stdin
|
|
24
|
+
// misbehaves under GJS the defect is in `@gjsify/fs` and belongs there, with a
|
|
25
|
+
// test — that is what made the branch removable in the first place
|
|
26
|
+
// (`packages/node/fs/src/fd-ops.spec.ts` + `tests/e2e/fs-std-fd`, which needs a
|
|
27
|
+
// real pipe and so cannot live in a unit spec).
|
|
30
28
|
import { readFileSync } from 'node:fs';
|
|
31
|
-
const CHUNK = 64 * 1024;
|
|
32
|
-
/** Read fd 0 to EOF under GJS via GioUnix (no `@gjsify/fs` fd support needed). */
|
|
33
|
-
function readStdinTextGioUnix() {
|
|
34
|
-
const gi = giNamespace();
|
|
35
|
-
const GioUnix = gi?.GioUnix;
|
|
36
|
-
if (typeof GioUnix?.InputStream !== 'function') {
|
|
37
|
-
throw new Error('GioUnix.InputStream is unavailable');
|
|
38
|
-
}
|
|
39
|
-
const stream = new GioUnix.InputStream({ fd: 0, close_fd: false });
|
|
40
|
-
const chunks = [];
|
|
41
|
-
let total = 0;
|
|
42
|
-
for (;;) {
|
|
43
|
-
const bytes = stream.read_bytes(CHUNK, null);
|
|
44
|
-
if (bytes.get_size() === 0)
|
|
45
|
-
break;
|
|
46
|
-
const arr = bytes.toArray();
|
|
47
|
-
chunks.push(arr);
|
|
48
|
-
total += arr.length;
|
|
49
|
-
}
|
|
50
|
-
if (total === 0)
|
|
51
|
-
return '';
|
|
52
|
-
const buf = new Uint8Array(total);
|
|
53
|
-
let offset = 0;
|
|
54
|
-
for (const c of chunks) {
|
|
55
|
-
buf.set(c, offset);
|
|
56
|
-
offset += c.length;
|
|
57
|
-
}
|
|
58
|
-
return new TextDecoder().decode(buf);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Read stdin under GJS through Gio ALONE, without GioUnix.
|
|
62
|
-
*
|
|
63
|
-
* `/dev/stdin` is fd 0 by another name, and `Gio` is present in every bundle
|
|
64
|
-
* that can reach this code at all. This exists because the GioUnix route has
|
|
65
|
-
* been observed to be unavailable on a host where it should have worked, and a
|
|
66
|
-
* second independent mechanism is cheaper than another round of guessing.
|
|
67
|
-
*/
|
|
68
|
-
function readStdinTextGioFile() {
|
|
69
|
-
const gi = giNamespace();
|
|
70
|
-
const Gio = gi?.Gio;
|
|
71
|
-
if (!Gio?.File)
|
|
72
|
-
throw new Error('Gio.File is unavailable');
|
|
73
|
-
const [ok, contents] = Gio.File.new_for_path('/dev/stdin').load_contents(null);
|
|
74
|
-
if (!ok)
|
|
75
|
-
throw new Error('Gio.File.load_contents("/dev/stdin") returned false');
|
|
76
|
-
return new TextDecoder().decode(contents);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The GJS `imports.gi` namespace, or `undefined` when not running on GJS.
|
|
80
|
-
* Same optional-chained probe as `isGjs()` — off GJS `imports` is simply
|
|
81
|
-
* absent and the read yields `undefined`; there is no throw path.
|
|
82
|
-
*/
|
|
83
|
-
function giNamespace() {
|
|
84
|
-
return globalThis.imports?.gi;
|
|
85
|
-
}
|
|
86
29
|
/**
|
|
87
30
|
* Read the whole of stdin as UTF-8. Works on Node and under the GJS bundle.
|
|
88
31
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `@gjsify/fs` has no numeric-fd path, so the `0` is coerced to a relative
|
|
93
|
-
* PATH and the call opens `./0`. A GJS run that reaches that line is already
|
|
94
|
-
* lost, so using it as GJS's fallback only converts a clear failure into
|
|
95
|
-
* `ENOENT … read '0'`, which is what a capability probe returning a false
|
|
96
|
-
* negative did.
|
|
97
|
-
*
|
|
98
|
-
* So: the presence of `imports.gi` decides. On GJS both GJS readers are tried
|
|
99
|
-
* and a failure THROWS with what was attempted; the Node reader is never
|
|
100
|
-
* reached. Off GJS there is no gi namespace and `readFileSync(0)` is the only
|
|
101
|
-
* and correct answer.
|
|
32
|
+
* Synchronous on purpose: the callers are CLI argument-parsing paths that run
|
|
33
|
+
* before any main loop exists, and the async `process.stdin` route carries the
|
|
34
|
+
* documented resume/pause races (see `utils/prompt.ts`).
|
|
102
35
|
*/
|
|
103
36
|
export function readStdinText() {
|
|
104
|
-
|
|
105
|
-
return readFileSync(0, 'utf-8');
|
|
106
|
-
const attempts = [];
|
|
107
|
-
for (const read of [readStdinTextGioUnix, readStdinTextGioFile]) {
|
|
108
|
-
try {
|
|
109
|
-
return read();
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
attempts.push(`${read.name}: ${err.message}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
throw new Error(`gjsify: cannot read stdin under GJS. Tried ${attempts.join('; ')}. ` +
|
|
116
|
-
'Pass the input as an argument instead (e.g. --base <ref> rather than --changed-from-stdin).');
|
|
37
|
+
return readFileSync(0, 'utf-8');
|
|
117
38
|
}
|
|
118
39
|
/**
|
|
119
40
|
* Read stdin and split it into trimmed, non-empty lines. Accepts LF and CRLF
|
package/lib/utils/system-gi.js
CHANGED
|
@@ -87,7 +87,16 @@
|
|
|
87
87
|
// bundle's first line runs.
|
|
88
88
|
import { spawnSync } from 'node:child_process';
|
|
89
89
|
import { statSync } from 'node:fs';
|
|
90
|
-
|
|
90
|
+
// POSIX path semantics, not the HOST's. This function answers a question about
|
|
91
|
+
// a darwin filesystem — `platform` is a PARAMETER, so the host running the code
|
|
92
|
+
// may be any of the three. Node's default `join`/`dirname`/`basename` follow the
|
|
93
|
+
// host, so on win32 `join('/usr/local/lib', 'girepository-1.0')` becomes
|
|
94
|
+
// `\usr\local\lib\girepository-1.0` and never matches the darwin path the
|
|
95
|
+
// caller (or the injected `existsDir`) is holding. Measured on the Windows leg:
|
|
96
|
+
// three `systemGiLibraryDirs` specs returned `[]` where they assert a prefix.
|
|
97
|
+
// `PROBED_GI_LIBDIRS` is darwin-only, so POSIX is always the right dialect here.
|
|
98
|
+
import { posix } from 'node:path';
|
|
99
|
+
const { basename, dirname, join } = posix;
|
|
91
100
|
/**
|
|
92
101
|
* Prefixes a macOS GI stack is installed under in practice — probed, never
|
|
93
102
|
* assumed: each is accepted only when it actually holds a `girepository-1.0/`.
|