@gjsify/cli 0.25.1 → 0.26.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 +171 -171
- package/lib/cli-app.js +7 -10
- package/lib/commands/affected.js +160 -17
- package/lib/commands/check.js +25 -8
- package/lib/commands/create.js +3 -1
- package/lib/commands/debug.js +3 -1
- package/lib/commands/flatpak/check.js +4 -3
- package/lib/commands/flatpak/diff.js +3 -1
- package/lib/commands/flatpak/sources.js +4 -7
- package/lib/commands/foreach.js +25 -7
- package/lib/commands/generate-installer.js +2 -4
- package/lib/commands/install.js +15 -7
- package/lib/commands/login.js +3 -1
- package/lib/commands/onboard.js +3 -6
- package/lib/commands/publish.js +8 -11
- package/lib/commands/self-update.js +4 -8
- package/lib/commands/showcase.d.ts +16 -0
- package/lib/commands/showcase.js +44 -24
- package/lib/commands/storybook.js +16 -2
- package/lib/commands/system-check.js +1 -2
- package/lib/commands/test.js +6 -2
- package/lib/commands/trust.js +12 -7
- package/lib/commands/tsc.js +3 -1
- package/lib/commands/uninstall.js +1 -2
- package/lib/commands/whoami.js +6 -3
- package/lib/utils/build-cache.js +7 -2
- package/lib/utils/dir-link.d.ts +58 -0
- package/lib/utils/dir-link.js +126 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +63 -10
- package/lib/utils/install-backend-native.js +1 -1
- package/lib/utils/install-lock.js +5 -7
- package/lib/utils/install-packument-cache.d.ts +23 -0
- package/lib/utils/install-packument-cache.js +45 -10
- package/lib/utils/install-tarball-cache.js +5 -8
- package/lib/utils/oxc-resolve.js +1 -1
- package/lib/utils/prompt.js +6 -1
- package/lib/utils/runtimes.d.ts +18 -0
- package/lib/utils/runtimes.js +20 -0
- package/lib/utils/stdin.js +6 -7
- package/package.json +17 -17
package/lib/utils/dlx-cache.d.ts
CHANGED
|
@@ -20,13 +20,40 @@ export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: numb
|
|
|
20
20
|
* Atomically swap `<cacheDir>/pkg` to point at `prepareDir`.
|
|
21
21
|
*
|
|
22
22
|
* Strategy:
|
|
23
|
-
* 1. Create new
|
|
24
|
-
* 2. `rename(pkg
|
|
23
|
+
* 1. Create new link `<cacheDir>/pkg.tmp-<ts>-<pid>` → prepareDir.
|
|
24
|
+
* 2. `rename(pkg.tmp-…, pkg)` — POSIX guarantees rename-over-existing is atomic.
|
|
25
25
|
*
|
|
26
26
|
* Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
|
|
27
27
|
* — a parallel process won, return its realpath.
|
|
28
|
+
*
|
|
29
|
+
* WINDOWS, both halves of which shipped broken:
|
|
30
|
+
*
|
|
31
|
+
* • Step 1 used `symlinkSync(…, 'dir')`, and a directory SYMLINK needs
|
|
32
|
+
* elevation or Developer Mode there, so a plain
|
|
33
|
+
* `npx @gjsify/cli@latest showcase …` died with `EPERM: operation not
|
|
34
|
+
* permitted, symlink`. It now goes through {@link linkDirSync}, which picks
|
|
35
|
+
* an NTFS junction — the same choice npm and yarn make, and the one
|
|
36
|
+
* `commands/install.ts` already made for workspace links without sharing it.
|
|
37
|
+
* • Step 2's rename-over-existing is a POSIX guarantee, not a Windows one:
|
|
38
|
+
* `MoveFileEx` will not replace an existing DIRECTORY, and a junction is a
|
|
39
|
+
* directory reparse point. So on the second run — when `pkg` already exists
|
|
40
|
+
* — the rename failed with `EPERM`, the catch below read that as "race
|
|
41
|
+
* lost", and the function returned the OLD target. That is not a crash but
|
|
42
|
+
* it is worse than one: the cache silently never refreshed. Windows
|
|
43
|
+
* therefore unlinks first and accepts the non-atomic window, which the
|
|
44
|
+
* existing race handling already covers.
|
|
45
|
+
*
|
|
46
|
+
* @param opts.junctions force the Windows link strategy (TESTS ONLY). Defaults
|
|
47
|
+
* to the host's. It is a parameter for the same reason `dirLinkTarget` takes
|
|
48
|
+
* `linkType`: nothing in CI runs on Windows, so an injected `true` is the ONLY
|
|
49
|
+
* way the unlink-then-rename ordering above is ever EXECUTED, and an
|
|
50
|
+
* unexecutable Windows branch is exactly how the `'dir'` bug shipped. What the
|
|
51
|
+
* injection proves off-host is the ORDERING and that the second swap really
|
|
52
|
+
* refreshes; what it cannot prove is `MoveFileEx`/junction syscall behaviour.
|
|
28
53
|
*/
|
|
29
|
-
export declare function symlinkSwap(cacheDir: string, prepareDir: string
|
|
54
|
+
export declare function symlinkSwap(cacheDir: string, prepareDir: string, opts?: {
|
|
55
|
+
junctions?: boolean;
|
|
56
|
+
}): string;
|
|
30
57
|
/** Clean up `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`. */
|
|
31
58
|
export declare function cleanupStalePrepareDirs(cacheDir: string, _maxAgeMinutes?: number): void;
|
|
32
59
|
/** Resolve absolute path to the installed package's directory inside cache. */
|
package/lib/utils/dlx-cache.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// - prepare into a fresh temp dir, then atomically swap a `pkg` symlink
|
|
7
7
|
// - TTL via lstat mtime + maxAgeMinutes (default 7 days)
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync
|
|
9
|
+
import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
|
+
import { dirLinksAreJunctions, linkDirSync } from './dir-link.js';
|
|
12
13
|
const ONE_MINUTE_MS = 60_000;
|
|
13
14
|
const DEFAULT_TTL_MIN = 60 * 24 * 7; // 7 days
|
|
14
15
|
function lexCompare(a, b) {
|
|
@@ -74,30 +75,82 @@ export function getValidCachedPkg(cacheDir, maxAgeMinutes = DEFAULT_TTL_MIN) {
|
|
|
74
75
|
* Atomically swap `<cacheDir>/pkg` to point at `prepareDir`.
|
|
75
76
|
*
|
|
76
77
|
* Strategy:
|
|
77
|
-
* 1. Create new
|
|
78
|
-
* 2. `rename(pkg
|
|
78
|
+
* 1. Create new link `<cacheDir>/pkg.tmp-<ts>-<pid>` → prepareDir.
|
|
79
|
+
* 2. `rename(pkg.tmp-…, pkg)` — POSIX guarantees rename-over-existing is atomic.
|
|
79
80
|
*
|
|
80
81
|
* Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
|
|
81
82
|
* — a parallel process won, return its realpath.
|
|
83
|
+
*
|
|
84
|
+
* WINDOWS, both halves of which shipped broken:
|
|
85
|
+
*
|
|
86
|
+
* • Step 1 used `symlinkSync(…, 'dir')`, and a directory SYMLINK needs
|
|
87
|
+
* elevation or Developer Mode there, so a plain
|
|
88
|
+
* `npx @gjsify/cli@latest showcase …` died with `EPERM: operation not
|
|
89
|
+
* permitted, symlink`. It now goes through {@link linkDirSync}, which picks
|
|
90
|
+
* an NTFS junction — the same choice npm and yarn make, and the one
|
|
91
|
+
* `commands/install.ts` already made for workspace links without sharing it.
|
|
92
|
+
* • Step 2's rename-over-existing is a POSIX guarantee, not a Windows one:
|
|
93
|
+
* `MoveFileEx` will not replace an existing DIRECTORY, and a junction is a
|
|
94
|
+
* directory reparse point. So on the second run — when `pkg` already exists
|
|
95
|
+
* — the rename failed with `EPERM`, the catch below read that as "race
|
|
96
|
+
* lost", and the function returned the OLD target. That is not a crash but
|
|
97
|
+
* it is worse than one: the cache silently never refreshed. Windows
|
|
98
|
+
* therefore unlinks first and accepts the non-atomic window, which the
|
|
99
|
+
* existing race handling already covers.
|
|
100
|
+
*
|
|
101
|
+
* @param opts.junctions force the Windows link strategy (TESTS ONLY). Defaults
|
|
102
|
+
* to the host's. It is a parameter for the same reason `dirLinkTarget` takes
|
|
103
|
+
* `linkType`: nothing in CI runs on Windows, so an injected `true` is the ONLY
|
|
104
|
+
* way the unlink-then-rename ordering above is ever EXECUTED, and an
|
|
105
|
+
* unexecutable Windows branch is exactly how the `'dir'` bug shipped. What the
|
|
106
|
+
* injection proves off-host is the ORDERING and that the second swap really
|
|
107
|
+
* refreshes; what it cannot prove is `MoveFileEx`/junction syscall behaviour.
|
|
82
108
|
*/
|
|
83
|
-
export function symlinkSwap(cacheDir, prepareDir) {
|
|
109
|
+
export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
|
|
110
|
+
const junctions = opts.junctions ?? dirLinksAreJunctions();
|
|
84
111
|
const linkPath = join(cacheDir, 'pkg');
|
|
85
112
|
const tmpName = `pkg.tmp-${Date.now().toString(16)}-${process.pid.toString(16)}`;
|
|
86
113
|
const tmpLink = join(cacheDir, tmpName);
|
|
87
114
|
// If we cannot even create the tmp link, give up (the error propagates).
|
|
88
|
-
|
|
115
|
+
linkDirSync(tmpLink, prepareDir);
|
|
89
116
|
try {
|
|
117
|
+
if (junctions) {
|
|
118
|
+
// Windows: no rename-over-a-directory. Removing first opens a window
|
|
119
|
+
// in which `pkg` is absent; a concurrent reader treats that as a cache
|
|
120
|
+
// miss and prepares its own, which is correct, just wasteful. force:true
|
|
121
|
+
// keeps a first-ever swap (nothing to remove) from throwing.
|
|
122
|
+
//
|
|
123
|
+
// maxRetries/retryDelay for the same reason `install-backend-native.ts`
|
|
124
|
+
// wraps every delete in `rmWithRetry`: on Windows a delete fails with
|
|
125
|
+
// EBUSY/EPERM while ANY process holds a handle (an antivirus scan, a
|
|
126
|
+
// concurrent reader that just realpath'd this very link), and the
|
|
127
|
+
// deletion then stays PENDING — so without the retry a transient
|
|
128
|
+
// handle sends us down the "race lost" branch and the cache silently
|
|
129
|
+
// does not refresh. Free on POSIX, where the first attempt succeeds.
|
|
130
|
+
rmSync(linkPath, { force: true, recursive: false, maxRetries: 10, retryDelay: 100 });
|
|
131
|
+
}
|
|
90
132
|
renameSync(tmpLink, linkPath);
|
|
91
133
|
}
|
|
92
134
|
catch (err) {
|
|
93
135
|
const code = err.code;
|
|
94
|
-
if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST') {
|
|
95
|
-
// Race lost — clean up our tmp and use whoever won.
|
|
136
|
+
if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST' || code === 'EACCES') {
|
|
137
|
+
// Race lost — clean up our tmp and use whoever won. force:true is
|
|
138
|
+
// the non-throwing spelling of "remove if still there"; any other
|
|
139
|
+
// failure on our own pid-unique link is real and should surface.
|
|
140
|
+
rmSync(tmpLink, { force: true });
|
|
96
141
|
try {
|
|
97
|
-
|
|
142
|
+
return realpathSync(linkPath);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Windows only, and only because the junction path unlinks before
|
|
146
|
+
// it renames: "race lost" can also mean the winner is INSIDE that
|
|
147
|
+
// non-atomic window, so `pkg` momentarily does not exist and this
|
|
148
|
+
// would throw ENOENT out of a function whose caller reports it to
|
|
149
|
+
// the user as `Could not resolve showcase "<name>"`. Our own
|
|
150
|
+
// prepareDir is a complete, correctly installed tree — a truthful
|
|
151
|
+
// answer for THIS run. The next run reads whichever link won.
|
|
152
|
+
return realpathSync(prepareDir);
|
|
98
153
|
}
|
|
99
|
-
catch { }
|
|
100
|
-
return realpathSync(linkPath);
|
|
101
154
|
}
|
|
102
155
|
throw err;
|
|
103
156
|
}
|
|
@@ -458,7 +458,7 @@ specOrigins) {
|
|
|
458
458
|
*
|
|
459
459
|
* This makes the current single-root-slot behavior honest instead of silent;
|
|
460
460
|
* the real fix — a per-workspace dedup pass that gives each conflicting
|
|
461
|
-
* requester its own nested copy — is Phase D.8 (see
|
|
461
|
+
* requester its own nested copy — is Phase D.8 (see status/open-todos.md).
|
|
462
462
|
*/
|
|
463
463
|
function emitTopLevelConflictWarnings(topLevelRanges, root) {
|
|
464
464
|
for (const [name, ranges] of topLevelRanges) {
|
|
@@ -84,13 +84,11 @@ function lockingDisabled() {
|
|
|
84
84
|
* we cannot prove abandoned; the mtime staleness budget still applies).
|
|
85
85
|
*/
|
|
86
86
|
function isPidAlive(pid) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/* fall through to the signal probe */
|
|
93
|
-
}
|
|
87
|
+
// `existsSync` never throws by contract: Node returns false on any error,
|
|
88
|
+
// and the GJS shim is a typeof guard + Gio `query_exists` (both without a
|
|
89
|
+
// throw path in the GIR). Off Linux the `if` is simply false.
|
|
90
|
+
if (existsSync('/proc/self'))
|
|
91
|
+
return existsSync(`/proc/${pid}`);
|
|
94
92
|
try {
|
|
95
93
|
process.kill(pid, 0);
|
|
96
94
|
return true;
|
|
@@ -3,6 +3,29 @@ export interface CachedPackument {
|
|
|
3
3
|
etag: string;
|
|
4
4
|
packument: Packument;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The cache key for a (registry, name) pair — exported so the filename rule
|
|
8
|
+
* below is a PROPERTY a test can assert, not a claim in a comment.
|
|
9
|
+
*
|
|
10
|
+
* THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
|
|
11
|
+
* `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
|
|
12
|
+
* is a Win32 RESERVED filename character, so every write failed with `EINVAL`.
|
|
13
|
+
* Nothing reported it: `atomicWrite` is best-effort on purpose (a read-only
|
|
14
|
+
* cache volume must not break an install) and `readCacheFile` reads an
|
|
15
|
+
* unreadable path as a MISS. So on Windows this cache was never broken loudly —
|
|
16
|
+
* it was silently DEAD, and the `If-None-Match` → `304` revalidation that is the
|
|
17
|
+
* entire reason this file exists never engaged once.
|
|
18
|
+
*
|
|
19
|
+
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
|
|
20
|
+
* a flat two-part name injective: an encoded part cannot contain the separator,
|
|
21
|
+
* so exactly one `(registry, name)` pair maps to any given filename.
|
|
22
|
+
*
|
|
23
|
+
* The shard key and the filename are derived from the SAME encoded parts. They
|
|
24
|
+
* used to disagree — the key hashed the raw pair while the name used the encoded
|
|
25
|
+
* one — which is how a reserved character in the name survived review of a
|
|
26
|
+
* function whose other half looked correct.
|
|
27
|
+
*/
|
|
28
|
+
export declare function packumentCacheKey(registry: string, name: string): string;
|
|
6
29
|
/**
|
|
7
30
|
* Read a cached packument + its ETag for `(registry, name)`. Returns `null` on
|
|
8
31
|
* a miss, when the cache is disabled, or when the entry is unreadable/corrupt
|
|
@@ -13,19 +13,22 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Layout:
|
|
15
15
|
//
|
|
16
|
-
// $XDG_CACHE_HOME/gjsify/metadata/
|
|
16
|
+
// $XDG_CACHE_HOME/gjsify/metadata/v2/<shard>/<encoded-registry>,<encoded-name>.json
|
|
17
17
|
//
|
|
18
18
|
// The cache is keyed by (registry, name) — NOT name alone — so switching the
|
|
19
19
|
// registry for a scope can never serve a packument from the wrong source on a
|
|
20
20
|
// coincidental ETag match. `<shard>` is a 2-hex FNV-1a digest of the key, a
|
|
21
21
|
// directory-fan-out step so the leaf dir never grows unbounded (same rationale
|
|
22
|
-
// as the tarball cache's hex sharding). `
|
|
22
|
+
// as the tarball cache's hex sharding). `v2` is a layout version.
|
|
23
23
|
//
|
|
24
24
|
// Disabled with `GJSIFY_PACKUMENT_CACHE=0` (or `false`). Honours
|
|
25
25
|
// `XDG_CACHE_HOME` like the tarball + dlx caches.
|
|
26
26
|
import { join } from 'node:path';
|
|
27
27
|
import { atomicWrite, gjsifyCacheRoot, readCacheFile } from './install-cache-fs.js';
|
|
28
|
-
|
|
28
|
+
// v2: the separator changed from the Win32-reserved `|` to `,` (see `pathFor`),
|
|
29
|
+
// so every v1 entry is unreachable rather than merely stale — a new root retires
|
|
30
|
+
// them instead of leaving orphans under the old one.
|
|
31
|
+
const CACHE_LAYOUT_VERSION = 'v2';
|
|
29
32
|
/** `true` unless `GJSIFY_PACKUMENT_CACHE` is `0` / `false` / empty. */
|
|
30
33
|
function isEnabled() {
|
|
31
34
|
const flag = process.env.GJSIFY_PACKUMENT_CACHE;
|
|
@@ -34,7 +37,7 @@ function isEnabled() {
|
|
|
34
37
|
const trimmed = flag.trim();
|
|
35
38
|
return !(trimmed === '0' || trimmed === 'false' || trimmed === '');
|
|
36
39
|
}
|
|
37
|
-
/** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/
|
|
40
|
+
/** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/v2`. */
|
|
38
41
|
function cacheRoot() {
|
|
39
42
|
return gjsifyCacheRoot('metadata', CACHE_LAYOUT_VERSION);
|
|
40
43
|
}
|
|
@@ -47,15 +50,47 @@ function shardFor(key) {
|
|
|
47
50
|
}
|
|
48
51
|
return ((h >>> 0) & 0xff).toString(16).padStart(2, '0');
|
|
49
52
|
}
|
|
50
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* One path part, made safe for EVERY filesystem we run on.
|
|
55
|
+
*
|
|
56
|
+
* `encodeURIComponent` alone is not enough: it deliberately leaves `*` (and
|
|
57
|
+
* `!'()`) unescaped, and `*` is one of Win32's reserved filename characters.
|
|
58
|
+
* A scoped package never contains one today, but a registry URL can, and the
|
|
59
|
+
* failure mode is the silent one described on {@link pathFor}.
|
|
60
|
+
*/
|
|
61
|
+
function encodePart(part) {
|
|
62
|
+
return encodeURIComponent(part).replace(/\*/g, '%2A');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The cache key for a (registry, name) pair — exported so the filename rule
|
|
66
|
+
* below is a PROPERTY a test can assert, not a claim in a comment.
|
|
67
|
+
*
|
|
68
|
+
* THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
|
|
69
|
+
* `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
|
|
70
|
+
* is a Win32 RESERVED filename character, so every write failed with `EINVAL`.
|
|
71
|
+
* Nothing reported it: `atomicWrite` is best-effort on purpose (a read-only
|
|
72
|
+
* cache volume must not break an install) and `readCacheFile` reads an
|
|
73
|
+
* unreadable path as a MISS. So on Windows this cache was never broken loudly —
|
|
74
|
+
* it was silently DEAD, and the `If-None-Match` → `304` revalidation that is the
|
|
75
|
+
* entire reason this file exists never engaged once.
|
|
76
|
+
*
|
|
77
|
+
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
|
|
78
|
+
* a flat two-part name injective: an encoded part cannot contain the separator,
|
|
79
|
+
* so exactly one `(registry, name)` pair maps to any given filename.
|
|
80
|
+
*
|
|
81
|
+
* The shard key and the filename are derived from the SAME encoded parts. They
|
|
82
|
+
* used to disagree — the key hashed the raw pair while the name used the encoded
|
|
83
|
+
* one — which is how a reserved character in the name survived review of a
|
|
84
|
+
* function whose other half looked correct.
|
|
85
|
+
*/
|
|
86
|
+
export function packumentCacheKey(registry, name) {
|
|
87
|
+
return `${encodePart(registry)},${encodePart(name)}`;
|
|
88
|
+
}
|
|
51
89
|
function pathFor(registry, name) {
|
|
52
90
|
if (!isEnabled())
|
|
53
91
|
return null;
|
|
54
|
-
const key =
|
|
55
|
-
|
|
56
|
-
// while staying a stable, reversible, single path segment.
|
|
57
|
-
const file = `${encodeURIComponent(registry)}|${encodeURIComponent(name)}.json`;
|
|
58
|
-
return join(cacheRoot(), shardFor(key), file);
|
|
92
|
+
const key = packumentCacheKey(registry, name);
|
|
93
|
+
return join(cacheRoot(), shardFor(key), `${key}.json`);
|
|
59
94
|
}
|
|
60
95
|
/**
|
|
61
96
|
* Read a cached packument + its ETag for `(registry, name)`. Returns `null` on
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// same hex bytes share the same cache entry; that is the invariant pnpm
|
|
31
31
|
// relies on too. Tarballs without an integrity hash (older registries)
|
|
32
32
|
// fall through to a no-op cache and download every time.
|
|
33
|
+
import { Buffer } from 'node:buffer';
|
|
33
34
|
import { existsSync, statSync } from 'node:fs';
|
|
34
35
|
import { homedir } from 'node:os';
|
|
35
36
|
import { join } from 'node:path';
|
|
@@ -53,14 +54,10 @@ function parseSri(integrity) {
|
|
|
53
54
|
return null;
|
|
54
55
|
const algorithm = integrity.slice(0, dashIdx);
|
|
55
56
|
const b64 = integrity.slice(dashIdx + 1).replace(/=+$/, '');
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
57
|
+
// Base64 decoding is lenient on every runtime (Node skips invalid chars,
|
|
58
|
+
// @gjsify/buffer's table decode maps them to 0): malformed input yields
|
|
59
|
+
// short/garbage output — caught by the length check below — never a throw.
|
|
60
|
+
const hex = Buffer.from(b64, 'base64').toString('hex');
|
|
64
61
|
if (hex.length < 4)
|
|
65
62
|
return null;
|
|
66
63
|
return { algorithm, hex };
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
// `gjsify/register-class-order` rule wired via `.oxlintrc.json` jsPlugins)
|
|
46
46
|
// lives in the Node launcher, so a native oxlint bridge could only run the
|
|
47
47
|
// Rust rule subset. Lint stays Node-spawned until that trade-off is decided
|
|
48
|
-
// (tracked in
|
|
48
|
+
// (tracked in status/open-todos.md).
|
|
49
49
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
50
50
|
import { join, resolve } from 'node:path';
|
|
51
51
|
import { createRequire } from 'node:module';
|
package/lib/utils/prompt.js
CHANGED
|
@@ -82,7 +82,12 @@ async function runRawSession(fn) {
|
|
|
82
82
|
out.write(r.echo);
|
|
83
83
|
if (r.interrupt) {
|
|
84
84
|
cleanup();
|
|
85
|
-
process.exit(
|
|
85
|
+
// `return` — a bare `process.exit()` is deferred under GJS (no
|
|
86
|
+
// atexit), so the loop kept consuming the rest of the chunk
|
|
87
|
+
// after Ctrl-C and could still resolve the pending prompt.
|
|
88
|
+
// Returning stops input handling here; the deferred exit then
|
|
89
|
+
// carries 130 on the way out.
|
|
90
|
+
return process.exit(130);
|
|
86
91
|
}
|
|
87
92
|
if (r.done) {
|
|
88
93
|
const { resolve } = pending;
|
package/lib/utils/runtimes.d.ts
CHANGED
|
@@ -26,6 +26,24 @@ export declare const RUNTIMES: Record<ExampleRuntime, RuntimeSpec>;
|
|
|
26
26
|
export declare function isExampleRuntime(value: string): value is ExampleRuntime;
|
|
27
27
|
/** The `gjsify build --app <target>` a runtime consumes (`gjs`, else `node`). */
|
|
28
28
|
export declare function buildAppForRuntime(runtime: ExampleRuntime): 'gjs' | 'node';
|
|
29
|
+
/**
|
|
30
|
+
* Does running an example/showcase on `runtime` require the GJS SYSTEM
|
|
31
|
+
* dependencies — i.e. a `gjs` binary on PATH?
|
|
32
|
+
*
|
|
33
|
+
* DERIVED from `buildApp`, deliberately not a second table: "consumes the
|
|
34
|
+
* `--app gjs` bundle" and "needs a gjs interpreter" are the same fact, and a
|
|
35
|
+
* hand-written second list drifts the first time a runtime is added.
|
|
36
|
+
*
|
|
37
|
+
* The caller must resolve the runtime BEFORE gating on it. `gjsify showcase`
|
|
38
|
+
* called `runMinimalChecks()` — which marks `gjs` `required` — three statements
|
|
39
|
+
* ABOVE the line resolving `args.runtime`, so `--runtime node` aborted with
|
|
40
|
+
* "Missing system dependencies: ✗ GJS" before reaching the `runtime !== 'gjs'`
|
|
41
|
+
* branch that never touches gjs. Every showcase was unreachable on a host
|
|
42
|
+
* without gjs (Windows, plain Node/bun/deno) — including the default path,
|
|
43
|
+
* since `defaultExampleRuntime()` falls back to the host runtime for exactly
|
|
44
|
+
* those hosts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function requiresGjsSystemDeps(runtime: ExampleRuntime): boolean;
|
|
29
47
|
/** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
|
|
30
48
|
export declare function isRuntimeOnPath(probe: string): boolean;
|
|
31
49
|
/**
|
package/lib/utils/runtimes.js
CHANGED
|
@@ -41,6 +41,26 @@ export function isExampleRuntime(value) {
|
|
|
41
41
|
export function buildAppForRuntime(runtime) {
|
|
42
42
|
return RUNTIMES[runtime].buildApp;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Does running an example/showcase on `runtime` require the GJS SYSTEM
|
|
46
|
+
* dependencies — i.e. a `gjs` binary on PATH?
|
|
47
|
+
*
|
|
48
|
+
* DERIVED from `buildApp`, deliberately not a second table: "consumes the
|
|
49
|
+
* `--app gjs` bundle" and "needs a gjs interpreter" are the same fact, and a
|
|
50
|
+
* hand-written second list drifts the first time a runtime is added.
|
|
51
|
+
*
|
|
52
|
+
* The caller must resolve the runtime BEFORE gating on it. `gjsify showcase`
|
|
53
|
+
* called `runMinimalChecks()` — which marks `gjs` `required` — three statements
|
|
54
|
+
* ABOVE the line resolving `args.runtime`, so `--runtime node` aborted with
|
|
55
|
+
* "Missing system dependencies: ✗ GJS" before reaching the `runtime !== 'gjs'`
|
|
56
|
+
* branch that never touches gjs. Every showcase was unreachable on a host
|
|
57
|
+
* without gjs (Windows, plain Node/bun/deno) — including the default path,
|
|
58
|
+
* since `defaultExampleRuntime()` falls back to the host runtime for exactly
|
|
59
|
+
* those hosts.
|
|
60
|
+
*/
|
|
61
|
+
export function requiresGjsSystemDeps(runtime) {
|
|
62
|
+
return buildAppForRuntime(runtime) === 'gjs';
|
|
63
|
+
}
|
|
44
64
|
/** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
|
|
45
65
|
export function isRuntimeOnPath(probe) {
|
|
46
66
|
try {
|
package/lib/utils/stdin.js
CHANGED
|
@@ -75,14 +75,13 @@ function readStdinTextGioFile() {
|
|
|
75
75
|
throw new Error('Gio.File.load_contents("/dev/stdin") returned false');
|
|
76
76
|
return new TextDecoder().decode(contents);
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
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
|
+
*/
|
|
79
83
|
function giNamespace() {
|
|
80
|
-
|
|
81
|
-
return globalThis.imports?.gi;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
84
|
+
return globalThis.imports?.gi;
|
|
86
85
|
}
|
|
87
86
|
/**
|
|
88
87
|
* Read the whole of stdin as UTF-8. Works on Node and under the GJS bundle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -130,19 +130,19 @@
|
|
|
130
130
|
"cli"
|
|
131
131
|
],
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@gjsify/buffer": "^0.
|
|
134
|
-
"@gjsify/create-app": "^0.
|
|
135
|
-
"@gjsify/node-globals": "^0.
|
|
136
|
-
"@gjsify/node-polyfills": "^0.
|
|
137
|
-
"@gjsify/npm-registry": "^0.
|
|
138
|
-
"@gjsify/resolve-npm": "^0.
|
|
139
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
140
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
141
|
-
"@gjsify/semver": "^0.
|
|
142
|
-
"@gjsify/tar": "^0.
|
|
143
|
-
"@gjsify/tsc": "^0.
|
|
144
|
-
"@gjsify/web-polyfills": "^0.
|
|
145
|
-
"@gjsify/workspace": "^0.
|
|
133
|
+
"@gjsify/buffer": "^0.26.1",
|
|
134
|
+
"@gjsify/create-app": "^0.26.1",
|
|
135
|
+
"@gjsify/node-globals": "^0.26.1",
|
|
136
|
+
"@gjsify/node-polyfills": "^0.26.1",
|
|
137
|
+
"@gjsify/npm-registry": "^0.26.1",
|
|
138
|
+
"@gjsify/resolve-npm": "^0.26.1",
|
|
139
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.26.1",
|
|
140
|
+
"@gjsify/rolldown-plugin-pnp": "^0.26.1",
|
|
141
|
+
"@gjsify/semver": "^0.26.1",
|
|
142
|
+
"@gjsify/tar": "^0.26.1",
|
|
143
|
+
"@gjsify/tsc": "^0.26.1",
|
|
144
|
+
"@gjsify/web-polyfills": "^0.26.1",
|
|
145
|
+
"@gjsify/workspace": "^0.26.1",
|
|
146
146
|
"cosmiconfig": "^9.0.2",
|
|
147
147
|
"get-tsconfig": "^4.14.0",
|
|
148
148
|
"pkg-types": "^2.3.1",
|
|
@@ -150,13 +150,13 @@
|
|
|
150
150
|
"yargs": "^18.0.0"
|
|
151
151
|
},
|
|
152
152
|
"devDependencies": {
|
|
153
|
-
"@gjsify/unit": "^0.
|
|
153
|
+
"@gjsify/unit": "^0.26.1",
|
|
154
154
|
"@types/yargs": "^17.0.35",
|
|
155
155
|
"typescript": "^6.0.3"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@gjsify/oxfmt-native": "^0.
|
|
159
|
-
"@gjsify/rolldown-native": "^0.
|
|
158
|
+
"@gjsify/oxfmt-native": "^0.26.1",
|
|
159
|
+
"@gjsify/rolldown-native": "^0.26.1"
|
|
160
160
|
},
|
|
161
161
|
"peerDependenciesMeta": {
|
|
162
162
|
"@gjsify/oxfmt-native": {
|