@gjsify/cli 0.36.0 → 0.38.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 +169 -169
- package/lib/actions/build.d.ts +7 -0
- package/lib/actions/build.js +18 -3
- package/lib/bundler-pick.js +61 -87
- package/lib/cli-fail.d.ts +4 -7
- package/lib/cli-fail.js +29 -55
- package/lib/commands/affected.js +161 -286
- package/lib/commands/install.d.ts +63 -66
- package/lib/commands/install.js +372 -536
- package/lib/config.d.ts +14 -0
- package/lib/config.js +18 -1
- package/lib/templates/install.mjs.tmpl +164 -75
- package/lib/types/cli-build-options.d.ts +33 -67
- package/lib/types/command.d.ts +9 -12
- package/lib/types/config-data.d.ts +137 -221
- package/lib/utils/auth-npmrc.d.ts +5 -10
- package/lib/utils/auth-npmrc.js +11 -21
- package/lib/utils/bin-shim.d.ts +40 -25
- package/lib/utils/bin-shim.js +51 -26
- package/lib/utils/clear-targets.d.ts +8 -11
- package/lib/utils/clear-targets.js +22 -29
- package/lib/utils/copy-targets.d.ts +13 -19
- package/lib/utils/copy-targets.js +31 -44
- package/lib/utils/dep-aggregation.d.ts +7 -15
- package/lib/utils/dep-aggregation.js +9 -22
- package/lib/utils/detect-native-packages.d.ts +89 -122
- package/lib/utils/detect-native-packages.js +119 -169
- package/lib/utils/dir-link.d.ts +29 -48
- package/lib/utils/dir-link.js +33 -56
- package/lib/utils/discover-showcases.d.ts +6 -7
- package/lib/utils/discover-showcases.js +11 -15
- package/lib/utils/dlx-cache.d.ts +29 -38
- package/lib/utils/dlx-cache.js +53 -63
- package/lib/utils/force-exit.d.ts +0 -3
- package/lib/utils/force-exit.js +13 -24
- package/lib/utils/gi-search-path.d.ts +16 -24
- package/lib/utils/gi-search-path.js +54 -82
- package/lib/utils/gjs-bundle-guard.d.ts +5 -13
- package/lib/utils/gjs-bundle-guard.js +35 -57
- package/lib/utils/gjs-source-escape.d.ts +2 -2
- package/lib/utils/gjs-source-escape.js +21 -39
- package/lib/utils/gjsify-shim.d.ts +17 -20
- package/lib/utils/gjsify-shim.js +80 -120
- package/lib/utils/html-entry.d.ts +8 -12
- package/lib/utils/html-entry.js +19 -28
- package/lib/utils/install-backend-native.d.ts +123 -161
- package/lib/utils/install-backend-native.js +536 -737
- package/lib/utils/install-backend.d.ts +42 -57
- package/lib/utils/install-backend.js +16 -21
- package/lib/utils/install-cache-fs.d.ts +10 -15
- package/lib/utils/install-cache-fs.js +18 -26
- package/lib/utils/install-lock.d.ts +5 -6
- package/lib/utils/install-lock.js +46 -65
- package/lib/utils/install-packument-cache.d.ts +27 -33
- package/lib/utils/install-packument-cache.js +44 -57
- package/lib/utils/install-progress.d.ts +2 -9
- package/lib/utils/install-progress.js +14 -39
- package/lib/utils/install-tarball-cache.d.ts +14 -20
- package/lib/utils/install-tarball-cache.js +41 -59
- package/lib/utils/library-output.d.ts +9 -22
- package/lib/utils/library-output.js +19 -37
- package/lib/utils/load-npmrc.d.ts +5 -6
- package/lib/utils/load-npmrc.js +18 -27
- package/lib/utils/node-bundle-guard.d.ts +4 -8
- package/lib/utils/node-bundle-guard.js +21 -35
- package/lib/utils/node-script.js +40 -53
- package/lib/utils/node-version.d.ts +5 -14
- package/lib/utils/node-version.js +15 -24
- package/lib/utils/normalize-bundler-options.d.ts +7 -11
- package/lib/utils/normalize-bundler-options.js +24 -42
- package/lib/utils/npm-oidc.d.ts +23 -37
- package/lib/utils/npm-oidc.js +28 -53
- package/lib/utils/npm-otp-cache.d.ts +4 -4
- package/lib/utils/npm-otp-cache.js +20 -25
- package/lib/utils/npm-otp.d.ts +20 -30
- package/lib/utils/npm-otp.js +32 -51
- package/lib/utils/onboard-probe.d.ts +7 -13
- package/lib/utils/onboard-probe.js +16 -27
- package/lib/utils/parse-spec.d.ts +3 -3
- package/lib/utils/parse-spec.js +4 -5
- package/lib/utils/pkg-json-edit.d.ts +7 -12
- package/lib/utils/pkg-json-edit.js +13 -26
- package/lib/utils/pkg-json.d.ts +11 -18
- package/lib/utils/pkg-json.js +19 -28
- package/lib/utils/prefixed-output.d.ts +3 -4
- package/lib/utils/prefixed-output.js +13 -19
- package/lib/utils/prompt.d.ts +3 -4
- package/lib/utils/prompt.js +14 -18
- package/lib/utils/publish-diagnose.d.ts +11 -16
- package/lib/utils/publish-diagnose.js +15 -28
- package/lib/utils/publish-headers.d.ts +7 -11
- package/lib/utils/publish-headers.js +36 -65
- package/lib/utils/publishable-packages.d.ts +4 -6
- package/lib/utils/publishable-packages.js +14 -18
- package/lib/utils/resolve-gjs-entry.d.ts +5 -8
- package/lib/utils/resolve-gjs-entry.js +10 -27
- package/lib/utils/resolve-npm-package.d.ts +6 -11
- package/lib/utils/resolve-npm-package.js +29 -63
- package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
- package/lib/utils/resolve-plugin-by-name.js +9 -14
- package/lib/utils/run-gjs.d.ts +16 -21
- package/lib/utils/run-gjs.js +31 -56
- package/lib/utils/run-lifecycle-script.d.ts +5 -9
- package/lib/utils/run-lifecycle-script.js +23 -42
- package/lib/utils/run-node.d.ts +21 -33
- package/lib/utils/run-node.js +43 -64
- package/lib/utils/runtimes.d.ts +25 -35
- package/lib/utils/runtimes.js +34 -46
- package/lib/utils/spawn.d.ts +32 -48
- package/lib/utils/spawn.js +51 -84
- package/lib/utils/stdin.d.ts +2 -2
- package/lib/utils/stdin.js +10 -25
- package/lib/utils/suggest.d.ts +6 -7
- package/lib/utils/suggest.js +8 -10
- package/lib/utils/system-gi.d.ts +36 -27
- package/lib/utils/system-gi.js +85 -114
- package/lib/utils/trust-registry.d.ts +4 -8
- package/lib/utils/trust-registry.js +7 -15
- package/lib/utils/win32-command.d.ts +8 -14
- package/lib/utils/win32-command.js +29 -43
- package/lib/utils/workspace-root.js +7 -12
- package/package.json +18 -18
package/lib/config.d.ts
CHANGED
|
@@ -10,5 +10,19 @@ export declare class Config {
|
|
|
10
10
|
private readPackageJSON;
|
|
11
11
|
/** Loads tsconfig.json of the current project */
|
|
12
12
|
private readTSConfig;
|
|
13
|
+
/**
|
|
14
|
+
* The globals policy for a `gjsify run --node-script` bundle, resolved from the
|
|
15
|
+
* package that OWNS THE SCRIPT — not the cwd, which during a monorepo build is the
|
|
16
|
+
* repo root. `gjsify.nodeScript` overrides either key (see `ConfigData.nodeScript`).
|
|
17
|
+
*
|
|
18
|
+
* CONFIG AND NOT A FLAG because on a host with no `node` the caller is a
|
|
19
|
+
* `package.json` spelling `node scripts/x.mjs`, re-entering the CLI through the shim
|
|
20
|
+
* `writeNodeShim()` writes: no call site to hang a flag on, on exactly the hosts this
|
|
21
|
+
* exists for.
|
|
22
|
+
*/
|
|
23
|
+
forNodeScript(scriptPath: string): Promise<{
|
|
24
|
+
globals?: string;
|
|
25
|
+
excludeGlobals?: string[];
|
|
26
|
+
}>;
|
|
13
27
|
forBuild(cliArgs: ArgumentsCamelCase<CliBuildOptions>): Promise<ConfigData>;
|
|
14
28
|
}
|
package/lib/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { APP_NAME } from './constants.js';
|
|
2
2
|
import { cosmiconfig } from 'cosmiconfig';
|
|
3
|
-
import { basename } from 'node:path';
|
|
3
|
+
import { basename, dirname } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
6
6
|
import { buildAppForRuntime } from './utils/runtimes.js';
|
|
@@ -216,6 +216,23 @@ export class Config {
|
|
|
216
216
|
const tsconfig = getTsconfig(dirPath)?.config || {};
|
|
217
217
|
return tsconfig;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The globals policy for a `gjsify run --node-script` bundle, resolved from the
|
|
221
|
+
* package that OWNS THE SCRIPT — not the cwd, which during a monorepo build is the
|
|
222
|
+
* repo root. `gjsify.nodeScript` overrides either key (see `ConfigData.nodeScript`).
|
|
223
|
+
*
|
|
224
|
+
* CONFIG AND NOT A FLAG because on a host with no `node` the caller is a
|
|
225
|
+
* `package.json` spelling `node scripts/x.mjs`, re-entering the CLI through the shim
|
|
226
|
+
* `writeNodeShim()` writes: no call site to hang a flag on, on exactly the hosts this
|
|
227
|
+
* exists for.
|
|
228
|
+
*/
|
|
229
|
+
async forNodeScript(scriptPath) {
|
|
230
|
+
const { config } = await this.load(dirname(scriptPath));
|
|
231
|
+
return {
|
|
232
|
+
globals: config.nodeScript?.globals ?? config.globals,
|
|
233
|
+
excludeGlobals: config.nodeScript?.excludeGlobals ?? config.excludeGlobals,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
219
236
|
async forBuild(cliArgs) {
|
|
220
237
|
const configFile = await this.load(process.cwd());
|
|
221
238
|
const configData = { ...configFile.config };
|
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env -S gjs -m
|
|
2
2
|
/**
|
|
3
|
-
* gjsify universal installer — bootstraps `@gjsify/cli` (or any GJS app
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* gjsify universal installer — bootstraps `@gjsify/cli` (or any GJS app on npm)
|
|
4
|
+
* on a system that has only `gjs` and libsoup: no Node, no npm. Decision and
|
|
5
|
+
* history: docs/adr/0002-bootstrap-bundle-minimization.md.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* gjs -m install.mjs --help
|
|
7
|
+
* Two stages: download a self-contained GJS bundle of `@gjsify/cli`
|
|
8
|
+
* (`cli.gjs.mjs`) from this repo's GitHub releases and verify its SHA-256, then
|
|
9
|
+
* spawn it as `gjs -m <bundle> install -g <target>@<tag>`. Dependency
|
|
10
|
+
* resolution, native prebuilds, lockfiles and the `~/.local/bin` launchers all
|
|
11
|
+
* live in that bundle — this bootstrapper re-implements none of it on purpose.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (`cli.gjs.mjs`) from this repo's GitHub releases. Verify SHA-256.
|
|
17
|
-
* 2. Spawn that bundle: `gjs -m <bundle> install -g <target>@<tag>`. The
|
|
18
|
-
* bundle handles transitive dependency resolution, native prebuilds,
|
|
19
|
-
* lockfiles, and the `~/.local/bin` launchers — all the things this
|
|
20
|
-
* thin bootstrapper deliberately does NOT re-implement.
|
|
13
|
+
* `gjsify generate-installer` re-emits this file for end-user GJS apps with the
|
|
14
|
+
* marked constants substituted.
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Test hooks (set by tests/e2e/install-script/run.mjs):
|
|
27
|
-
* GJSIFY_INSTALL_BOOTSTRAP_URL override the cli.gjs.mjs download origin
|
|
28
|
-
* (accepts file:// for offline tests)
|
|
29
|
-
* GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL override the .sha256 companion URL.
|
|
30
|
-
* An EMPTY string is the only way to install
|
|
31
|
-
* without verification, and it warns loudly; a
|
|
32
|
-
* digest URL that is set but unreachable is a
|
|
33
|
-
* hard failure, never a silent downgrade.
|
|
34
|
-
* GJSIFY_GLOBAL_PREFIX override install prefix (forwarded to cli)
|
|
35
|
-
* GJSIFY_GLOBAL_BIN_DIR override bin dir (forwarded to cli)
|
|
36
|
-
* GJSIFY_INSTALL_REGISTRY override npm registry (forwarded as npm_config_registry)
|
|
37
|
-
* GJSIFY_INSTALL_BOOTSTRAP_CACHE override the bootstrap cache dir
|
|
16
|
+
* `--help` documents the user-facing flags and env vars. Two more exist only for
|
|
17
|
+
* tests/e2e/install-script/run.mjs: GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL and
|
|
18
|
+
* GJSIFY_INSTALL_BOOTSTRAP_CACHE.
|
|
38
19
|
*/
|
|
39
20
|
|
|
40
21
|
import GLib from 'gi://GLib';
|
|
@@ -53,17 +34,16 @@ const DEFAULT_BOOTSTRAP_SHA256_URL = `${DEFAULT_BOOTSTRAP_URL}.sha256`;
|
|
|
53
34
|
|
|
54
35
|
const USER_AGENT = 'gjsify-installer/1.0';
|
|
55
36
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
37
|
+
// Content-addressed cache: `cli-<sha256>.gjs.mjs`. Why the digest and not a
|
|
38
|
+
// fixed name keys it: `resolveBootstrap()`.
|
|
58
39
|
const CACHE_PREFIX = 'cli-';
|
|
59
40
|
const CACHE_SUFFIX = '.gjs.mjs';
|
|
60
41
|
|
|
61
42
|
/**
|
|
62
|
-
* Under `--fetch-only`
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* surfaces as a confusing "file not found" rather than as bad output.
|
|
43
|
+
* Under `--fetch-only` stdout carries nothing but the bundle path, so progress
|
|
44
|
+
* goes to stderr. A caller does `BOOTSTRAP=$(gjs -m install.mjs --fetch-only)`,
|
|
45
|
+
* where one stray `info()` line glues a log message to the front of the path and
|
|
46
|
+
* surfaces later as a confusing "file not found".
|
|
67
47
|
*/
|
|
68
48
|
let logToStderr = false;
|
|
69
49
|
|
|
@@ -139,7 +119,7 @@ Examples:
|
|
|
139
119
|
}
|
|
140
120
|
|
|
141
121
|
function checkGjsVersion() {
|
|
142
|
-
//
|
|
122
|
+
// `system.version` is packed: major*10000 + minor*100 + micro
|
|
143
123
|
const v = system?.version;
|
|
144
124
|
if (typeof v !== 'number') return;
|
|
145
125
|
const major = Math.floor(v / 10000);
|
|
@@ -154,6 +134,62 @@ function checkGjsVersion() {
|
|
|
154
134
|
}
|
|
155
135
|
}
|
|
156
136
|
|
|
137
|
+
/** Total attempts per URL, and the wait before each retry. Override for tests / slow links. */
|
|
138
|
+
const FETCH_ATTEMPTS = Number(GLib.getenv('GJSIFY_INSTALL_FETCH_ATTEMPTS') || '5');
|
|
139
|
+
const RETRY_BACKOFF_MS = [1000, 2000, 4000, 8000];
|
|
140
|
+
|
|
141
|
+
function sleepMs(ms) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
GLib.timeout_add(GLib.PRIORITY_DEFAULT, ms, () => {
|
|
144
|
+
resolve();
|
|
145
|
+
return GLib.SOURCE_REMOVE;
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fetchOnce(session, url, { forceHttp1 = false } = {}) {
|
|
151
|
+
const message = Soup.Message.new('GET', url);
|
|
152
|
+
message.request_headers.append('User-Agent', USER_AGENT);
|
|
153
|
+
// The failure this retries against announces its own protocol — libsoup reports it
|
|
154
|
+
// verbatim as `HTTP/2 Error: NO_ERROR`, a stream closed mid-response. Retrying over
|
|
155
|
+
// HTTP/1.1 sidesteps the multiplexing that produces it, so the first attempt keeps the
|
|
156
|
+
// faster protocol and every retry drops to the one that cannot fail that way. Guarded:
|
|
157
|
+
// `set_force_http1` is libsoup 3.4+, and this script must still run on older hosts.
|
|
158
|
+
if (forceHttp1 && typeof message.set_force_http1 === 'function') message.set_force_http1(true);
|
|
159
|
+
const bytes = await session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null);
|
|
160
|
+
const status = message.get_status();
|
|
161
|
+
if (status !== Soup.Status.OK) {
|
|
162
|
+
const err = new Error(`HTTP ${status} from ${url}`);
|
|
163
|
+
err.httpStatus = status;
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
return bytes.get_data();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A dropped connection is a HICCUP; an HTTP answer is an ANSWER.
|
|
171
|
+
*
|
|
172
|
+
* THE INCIDENT. `https://github.com/gjsify/gjsify/releases/latest/download/…` dropped
|
|
173
|
+
* HTTP/2 connections for a stretch on 2026-08-12 and took four CI jobs across two PRs
|
|
174
|
+
* with it, each dying in `gjsify-setup` before a line of the change under test ran.
|
|
175
|
+
* Measured from a workstation during the same window: one in three `curl` attempts
|
|
176
|
+
* returned `Connection died, tried 5 times before giving up`. A single-shot fetch turns
|
|
177
|
+
* that into "Refusing to install an UNVERIFIED bootstrap bundle", which reads like the
|
|
178
|
+
* digest is missing or hostile — the one message that must never be a false alarm, since
|
|
179
|
+
* the correct response to it is to disable verification.
|
|
180
|
+
*
|
|
181
|
+
* So retries are scoped to what CANNOT be an answer: a transport failure (no status at
|
|
182
|
+
* all), plus 429 and 5xx. A 404 is the registry saying the asset does not exist and a 403
|
|
183
|
+
* is it saying no; retrying either would only delay the same result while making a real
|
|
184
|
+
* outage look like a hang.
|
|
185
|
+
*
|
|
186
|
+
* FIVE attempts, not three, and the numbers come from a run rather than a preference:
|
|
187
|
+
* with three the CI log showed `retry 2/3` and `retry 3/3` and still failed, because each
|
|
188
|
+
* attempt takes ~20 s to give up — so the 500ms/1500ms backoff was noise beside it and the
|
|
189
|
+
* whole sequence covered barely 40 s of a longer wobble. 1/2/4/8 s over five attempts
|
|
190
|
+
* covers about two minutes. Beyond that it is an outage, not a hiccup, and waiting longer
|
|
191
|
+
* only turns a clear failure into a hang.
|
|
192
|
+
*/
|
|
157
193
|
async function fetchBytes(session, url) {
|
|
158
194
|
if (url.startsWith('file://')) {
|
|
159
195
|
const path = url.slice('file://'.length);
|
|
@@ -161,14 +197,22 @@ async function fetchBytes(session, url) {
|
|
|
161
197
|
const [, bytes] = file.load_contents(null);
|
|
162
198
|
return bytes;
|
|
163
199
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
const attempts = Number.isFinite(FETCH_ATTEMPTS) && FETCH_ATTEMPTS >= 1 ? FETCH_ATTEMPTS : 1;
|
|
201
|
+
let lastErr = null;
|
|
202
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
203
|
+
try {
|
|
204
|
+
return await fetchOnce(session, url, { forceHttp1: attempt > 1 });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
lastErr = err;
|
|
207
|
+
const status = err.httpStatus;
|
|
208
|
+
const retriable = status === undefined || status === 429 || status >= 500;
|
|
209
|
+
if (!retriable || attempt === attempts) break;
|
|
210
|
+
const wait = RETRY_BACKOFF_MS[Math.min(attempt - 1, RETRY_BACKOFF_MS.length - 1)];
|
|
211
|
+
info(`Fetch of ${url} failed (${err.message}) — retry ${attempt + 1}/${attempts} in ${wait}ms`);
|
|
212
|
+
await sleepMs(wait);
|
|
213
|
+
}
|
|
170
214
|
}
|
|
171
|
-
|
|
215
|
+
throw lastErr;
|
|
172
216
|
}
|
|
173
217
|
|
|
174
218
|
function sha256Hex(bytes) {
|
|
@@ -197,18 +241,17 @@ function readBytesOrNull(path) {
|
|
|
197
241
|
const [, bytes] = Gio.File.new_for_path(path).load_contents(null);
|
|
198
242
|
return bytes;
|
|
199
243
|
} catch {
|
|
200
|
-
// An absent
|
|
201
|
-
//
|
|
202
|
-
//
|
|
244
|
+
// An absent or unreadable entry (truncated by a killed earlier run) is a
|
|
245
|
+
// MISS, not an error. `load_contents` is `throws="1"`, so this catch has
|
|
246
|
+
// a genuine throw path.
|
|
203
247
|
return null;
|
|
204
248
|
}
|
|
205
249
|
}
|
|
206
250
|
|
|
207
251
|
/**
|
|
208
|
-
* Drop every other cached bootstrap
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* can never be warm" for "the cache grows forever".
|
|
252
|
+
* Drop every other cached bootstrap. Each entry is a ~6.6 MB bundle and the
|
|
253
|
+
* digest changes every release, so without pruning, content-addressing would
|
|
254
|
+
* trade "the cache can never be warm" for "the cache grows forever".
|
|
212
255
|
*/
|
|
213
256
|
function pruneCache(dir, keepBasename) {
|
|
214
257
|
const children = Gio.File.new_for_path(dir).enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
|
@@ -221,24 +264,53 @@ function pruneCache(dir, keepBasename) {
|
|
|
221
264
|
}
|
|
222
265
|
}
|
|
223
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The newest cached bootstrap whose CONTENTS hash to the digest in its own name, or
|
|
269
|
+
* null. Re-hashing is the whole point: the filename is a claim, and a truncated entry
|
|
270
|
+
* keeps its name — the same reason the warm-cache path below re-hashes rather than
|
|
271
|
+
* trusting it.
|
|
272
|
+
*/
|
|
273
|
+
function newestCachedBootstrap(dir) {
|
|
274
|
+
let best = null;
|
|
275
|
+
let children;
|
|
276
|
+
try {
|
|
277
|
+
children = Gio.File.new_for_path(dir).enumerate_children(
|
|
278
|
+
'standard::name,time::modified',
|
|
279
|
+
Gio.FileQueryInfoFlags.NONE,
|
|
280
|
+
null,
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
// No cache directory yet — `enumerate_children` is `throws="1"`.
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
for (;;) {
|
|
287
|
+
const entry = children.next_file(null);
|
|
288
|
+
if (entry === null) break;
|
|
289
|
+
const name = entry.get_name();
|
|
290
|
+
if (!name.startsWith(CACHE_PREFIX) || !name.endsWith(CACHE_SUFFIX)) continue;
|
|
291
|
+
const claimed = name.slice(CACHE_PREFIX.length, name.length - CACHE_SUFFIX.length).toLowerCase();
|
|
292
|
+
if (!/^[0-9a-f]{64}$/.test(claimed)) continue;
|
|
293
|
+
const path = GLib.build_filenamev([dir, name]);
|
|
294
|
+
const bytes = readBytesOrNull(path);
|
|
295
|
+
if (!bytes || sha256Hex(bytes).toLowerCase() !== claimed) continue;
|
|
296
|
+
const mtime = entry.get_modification_date_time()?.to_unix() ?? 0;
|
|
297
|
+
if (!best || mtime > best.mtime) best = { path, basename: name, mtime };
|
|
298
|
+
}
|
|
299
|
+
return best;
|
|
300
|
+
}
|
|
301
|
+
|
|
224
302
|
/**
|
|
225
303
|
* Resolve a VERIFIED bootstrap bundle and return its path.
|
|
226
304
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* a 404, or a captive portal silently downgraded the install to no
|
|
235
|
-
* verification at all. Fetching the digest first makes that impossible:
|
|
236
|
-
* no digest, no install.
|
|
237
|
-
* 2. The cache could never be warm. It wrote a fixed `cli.gjs.mjs` and never
|
|
238
|
-
* read it back, so every run re-downloaded ~6.6 MB. A content-addressed
|
|
239
|
-
* name lets a matching entry short-circuit the download entirely.
|
|
305
|
+
* Fetching the digest BEFORE the bundle, and keying the cache by it, is the
|
|
306
|
+
* mechanism rather than an optimisation; the reverse order cost two defects.
|
|
307
|
+
* Verifying after downloading made verification skippable by anyone who could
|
|
308
|
+
* break ONE request — a failed `.sha256` fetch logged "skipping verification"
|
|
309
|
+
* and carried on, so a proxy, a 404 or a captive portal silently downgraded the
|
|
310
|
+
* install. And a fixed `cli.gjs.mjs` name that was never read back meant every
|
|
311
|
+
* run re-downloaded ~6.6 MB; a content-addressed name short-circuits that.
|
|
240
312
|
*
|
|
241
|
-
* Skipping verification
|
|
313
|
+
* Skipping verification remains possible only as an EXPLICIT act
|
|
242
314
|
* (`GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL=''`), and it says so on stderr.
|
|
243
315
|
*/
|
|
244
316
|
async function resolveBootstrap(session, bootstrapUrl, sha256Url) {
|
|
@@ -249,6 +321,26 @@ async function resolveBootstrap(session, bootstrapUrl, sha256Url) {
|
|
|
249
321
|
try {
|
|
250
322
|
sumBytes = await fetchBytes(session, sha256Url);
|
|
251
323
|
} catch (err) {
|
|
324
|
+
// Before refusing: a bundle already in the cache carries its own digest in
|
|
325
|
+
// its NAME, and re-hashing the bytes proves the name. Those bytes were
|
|
326
|
+
// verified against a digest that WAS fetched, on an earlier run — so reusing
|
|
327
|
+
// them installs nothing unverified. That is a different thing from the defect
|
|
328
|
+
// this function's header records, where a failed digest fetch waved a FRESH
|
|
329
|
+
// download through with no digest at all.
|
|
330
|
+
//
|
|
331
|
+
// What it does concede is FRESHNESS: someone who can block the network can
|
|
332
|
+
// hold you on the release you already have. So it is loud, it names the
|
|
333
|
+
// digest, and it only ever runs after the retries above are exhausted — which
|
|
334
|
+
// on 2026-08-12 was a GitHub release CDN dropping every connection from the
|
|
335
|
+
// CI runners for over an hour, with a warm cache sitting right there, unusable
|
|
336
|
+
// because the digest fetch comes first.
|
|
337
|
+
const cached = newestCachedBootstrap(cacheDir());
|
|
338
|
+
if (cached) {
|
|
339
|
+
error(`Could not fetch ${sha256Url}: ${err.message}`);
|
|
340
|
+
info(`Falling back to the cached bootstrap verified earlier: ${cached.basename}`);
|
|
341
|
+
info('It may be OLDER than `latest` — re-run once the network recovers.');
|
|
342
|
+
return cached.path;
|
|
343
|
+
}
|
|
252
344
|
error(`Could not fetch ${sha256Url}: ${err.message}`);
|
|
253
345
|
error('Refusing to install an UNVERIFIED bootstrap bundle.');
|
|
254
346
|
error("To bootstrap without verification, set GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL='' explicitly.");
|
|
@@ -313,14 +405,12 @@ function buildSpec(target, tag) {
|
|
|
313
405
|
}
|
|
314
406
|
|
|
315
407
|
async function runInstall(bundlePath, spec) {
|
|
316
|
-
// `gjsify install -g <spec>`
|
|
317
|
-
//
|
|
318
|
-
// installer's own `--force` is satisfied by the fact that we always
|
|
319
|
-
// re-download the bootstrap and re-invoke the CLI.
|
|
408
|
+
// `gjsify install -g <spec>` rewrites the tree unconditionally, so there is
|
|
409
|
+
// no force flag to forward and `--force` needs nothing here.
|
|
320
410
|
info(`Running: gjs -m <bootstrap> install -g ${spec}`);
|
|
321
411
|
const argv = ['gjs', '-m', bundlePath, 'install', '-g', spec];
|
|
322
|
-
//
|
|
323
|
-
// reach the spawned CLI.
|
|
412
|
+
// No `envp`: the launcher inherits ours, so override env vars set by tests
|
|
413
|
+
// and power-users reach the spawned CLI.
|
|
324
414
|
const launcher = new Gio.SubprocessLauncher({
|
|
325
415
|
flags: Gio.SubprocessFlags.NONE,
|
|
326
416
|
});
|
|
@@ -349,7 +439,6 @@ async function main() {
|
|
|
349
439
|
}
|
|
350
440
|
|
|
351
441
|
if (opts.fetchOnly) {
|
|
352
|
-
// The one thing on stdout. Everything above went to stderr.
|
|
353
442
|
print(bundlePath);
|
|
354
443
|
exit(0);
|
|
355
444
|
}
|
|
@@ -1,106 +1,72 @@
|
|
|
1
1
|
import type { App } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
2
|
export interface CliBuildOptions {
|
|
3
|
-
/**
|
|
4
|
-
* This is an array of files that each serve as an input to the bundling algorithm.
|
|
5
|
-
* @see https://esbuild.github.io/api/#entry-points
|
|
6
|
-
*/
|
|
3
|
+
/** Input files to the bundling algorithm. */
|
|
7
4
|
entryPoints?: string[];
|
|
8
|
-
/** Switch on the verbose mode */
|
|
9
5
|
verbose?: boolean;
|
|
10
|
-
/**
|
|
11
|
-
* When enabled, the generated code will be minified instead of pretty-printed.
|
|
12
|
-
* @see https://esbuild.github.io/api/#minify
|
|
13
|
-
*/
|
|
6
|
+
/** Minify the generated code instead of pretty-printing it. */
|
|
14
7
|
minify?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
* Override the default output format.
|
|
17
|
-
* @see https://esbuild.github.io/api/#format
|
|
18
|
-
*/
|
|
8
|
+
/** Override the default output format. */
|
|
19
9
|
format?: 'iife' | 'esm' | 'cjs';
|
|
20
|
-
/**
|
|
10
|
+
/** Build an application or test. `node` is mostly internal, for gjsify's tests. */
|
|
21
11
|
app?: App;
|
|
22
|
-
/**
|
|
12
|
+
/** Build a library rather than an application. */
|
|
23
13
|
library?: boolean;
|
|
24
|
-
/**
|
|
25
|
-
* This option sets the output file name for the build operation.
|
|
26
|
-
* This is only applicable if there is a single entry point.
|
|
27
|
-
* If there are multiple entry points, you must use the outdir option instead to specify an output directory.
|
|
28
|
-
* @see https://esbuild.github.io/api/#outfile
|
|
29
|
-
*/
|
|
14
|
+
/** Output file name. Single entry point only — otherwise use `outdir`. */
|
|
30
15
|
outfile?: string;
|
|
31
|
-
/**
|
|
32
|
-
* This option sets the output directory for the build operation.
|
|
33
|
-
* @see https://esbuild.github.io/api/#outdir
|
|
34
|
-
*/
|
|
35
16
|
outdir?: string;
|
|
36
|
-
/** Enables TypeScript types
|
|
17
|
+
/** Enables TypeScript types at runtime using Deepkit's type compiler. */
|
|
37
18
|
reflection?: boolean;
|
|
38
|
-
/**
|
|
39
|
-
* The log level can be changed to prevent esbuild from printing warning and/or error messages to the terminal
|
|
40
|
-
* @see https://esbuild.github.io/api/#log-level
|
|
41
|
-
*/
|
|
19
|
+
/** How much the bundler prints to the terminal. */
|
|
42
20
|
logLevel: 'silent' | 'error' | 'warning' | 'info' | 'debug' | 'verbose';
|
|
43
|
-
/**
|
|
21
|
+
/** Glob patterns excluding matches and aliases. */
|
|
44
22
|
exclude?: string[];
|
|
45
23
|
/**
|
|
46
|
-
* Inject a console shim into GJS builds for clean output (no GLib prefix,
|
|
47
|
-
*
|
|
24
|
+
* Inject a console shim into GJS app builds for clean output (no GLib prefix,
|
|
25
|
+
* working ANSI colors). Disable with `--no-console-shim`. Default: true.
|
|
48
26
|
*/
|
|
49
27
|
consoleShim?: boolean;
|
|
50
28
|
/**
|
|
51
|
-
* Comma-separated
|
|
52
|
-
* `"fetch,Buffer,process,URL,crypto"
|
|
53
|
-
*
|
|
54
|
-
* bundle. Only applies to GJS app builds.
|
|
29
|
+
* Comma-separated global identifiers the code needs, e.g.
|
|
30
|
+
* `"fetch,Buffer,process,URL,crypto"`. Each maps to the corresponding
|
|
31
|
+
* `@gjsify/<pkg>/register` module, injected into the bundle. GJS app builds only.
|
|
55
32
|
*/
|
|
56
33
|
globals?: string;
|
|
57
34
|
/**
|
|
58
|
-
* Prepend a `#!/usr/bin/env -S gjs -m` shebang
|
|
59
|
-
*
|
|
60
|
-
* `--outfile`. Default: false.
|
|
35
|
+
* Prepend a `#!/usr/bin/env -S gjs -m` shebang and chmod 755. GJS app builds with
|
|
36
|
+
* a single `--outfile` only. Default: false.
|
|
61
37
|
*/
|
|
62
38
|
shebang?: boolean;
|
|
63
39
|
/**
|
|
64
|
-
* Module names that
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* `--external typedoc,prettier --external typescript`. Glob-style wildcards
|
|
70
|
-
* (`@inquirer/*`, `lodash-*`) are forwarded as-is to esbuild.
|
|
71
|
-
*
|
|
72
|
-
* @see https://esbuild.github.io/api/#external
|
|
40
|
+
* Module names that must NOT be bundled: each stays a literal `import`/`require`
|
|
41
|
+
* in the output, resolved by the runtime at execution time. Repeat the flag or
|
|
42
|
+
* pass a comma-separated value (`--external typedoc,prettier --external
|
|
43
|
+
* typescript`); glob-style wildcards (`@inquirer/*`, `lodash-*`) pass through to
|
|
44
|
+
* the bundler unchanged.
|
|
73
45
|
*/
|
|
74
46
|
external?: string[];
|
|
75
47
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* upstream packages that read a build-time constant via
|
|
48
|
+
* Compile-time constants as `KEY=VALUE`, where `VALUE` is an arbitrary JS
|
|
49
|
+
* expression — string literals must be quoted (`--define VERSION='"1.2.3"'`).
|
|
50
|
+
* For upstream packages reading a build-time constant via
|
|
80
51
|
* `typeof __FOO__ !== 'undefined'`.
|
|
81
|
-
*
|
|
82
|
-
* @see https://esbuild.github.io/api/#define
|
|
83
52
|
*/
|
|
84
53
|
define?: string[];
|
|
85
54
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* scenario never executes). Layered on top of the built-in alias map.
|
|
55
|
+
* Remap module specifiers at bundle time as `FROM=TO`, typically to
|
|
56
|
+
* `@gjsify/empty` to drop a heavy dep the scenario never executes. Layered on top
|
|
57
|
+
* of the built-in alias map.
|
|
90
58
|
*/
|
|
91
59
|
alias?: string[];
|
|
92
60
|
/**
|
|
93
|
-
* Comma-separated global identifiers to
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* Example: `--exclude-globals fetch,XMLHttpRequest`
|
|
61
|
+
* Comma-separated global identifiers to drop from the auto-detected set — for
|
|
62
|
+
* false positives from dead browser-compat code in npm deps whose polyfills need
|
|
63
|
+
* unavailable native libraries. Example: `--exclude-globals fetch,XMLHttpRequest`.
|
|
97
64
|
*/
|
|
98
65
|
excludeGlobals?: string[];
|
|
99
66
|
/**
|
|
100
|
-
* Watch
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* npm `rolldown` engine — run under Node.
|
|
67
|
+
* Watch and rebuild on change; SIGINT/SIGTERM closes the watcher cleanly. Valid
|
|
68
|
+
* only with `--app gjs|node|browser`, rejected with `--library`, and requires the
|
|
69
|
+
* npm `rolldown` engine — so run it under Node.
|
|
104
70
|
*/
|
|
105
71
|
watch?: boolean;
|
|
106
72
|
}
|
package/lib/types/command.d.ts
CHANGED
|
@@ -8,19 +8,16 @@ export interface Command<T = unknown, U = T> {
|
|
|
8
8
|
deprecated?: boolean | string;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* A command that definitely runs something —
|
|
12
|
-
*
|
|
11
|
+
* A command that definitely runs something — every command except the
|
|
12
|
+
* builder-only parents. Annotate any command a standalone entry invokes directly.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* to the import closure of `src/index.ts`. Annotate a command that a standalone
|
|
22
|
-
* entry invokes directly with this type, and the guarantee is in the type
|
|
23
|
-
* instead of in a cast.
|
|
14
|
+
* `Command.handler` must stay optional for parent commands, but that optionality
|
|
15
|
+
* then lands on every CALL site, including those outside `cli-app.ts`'s yargs
|
|
16
|
+
* registration, where a missing handler is a silent no-op rather than a type
|
|
17
|
+
* error. `src/affected-entry.ts` — the entry `dist/affected.gjs.mjs` is built
|
|
18
|
+
* from — invoked `affectedCommand.handler(...)` through a cast that suppressed
|
|
19
|
+
* exactly that, unnoticed for as long as the tsconfig scoped the compile to the
|
|
20
|
+
* import closure of `src/index.ts`. This puts the guarantee in the type instead.
|
|
24
21
|
*/
|
|
25
22
|
export type LeafCommand<T = unknown, U = T> = Command<T, U> & {
|
|
26
23
|
handler: NonNullable<Command<T, U>['handler']>;
|