@gjsify/cli 0.36.0 → 0.37.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.
Files changed (116) hide show
  1. package/dist/cli.gjs.mjs +3 -3
  2. package/lib/bundler-pick.js +61 -87
  3. package/lib/cli-fail.d.ts +4 -7
  4. package/lib/cli-fail.js +29 -55
  5. package/lib/commands/affected.js +161 -286
  6. package/lib/commands/install.d.ts +63 -66
  7. package/lib/commands/install.js +372 -536
  8. package/lib/templates/install.mjs.tmpl +38 -68
  9. package/lib/types/cli-build-options.d.ts +33 -67
  10. package/lib/types/command.d.ts +9 -12
  11. package/lib/types/config-data.d.ts +125 -221
  12. package/lib/utils/auth-npmrc.d.ts +5 -10
  13. package/lib/utils/auth-npmrc.js +11 -21
  14. package/lib/utils/clear-targets.d.ts +8 -11
  15. package/lib/utils/clear-targets.js +22 -29
  16. package/lib/utils/copy-targets.d.ts +13 -19
  17. package/lib/utils/copy-targets.js +31 -44
  18. package/lib/utils/dep-aggregation.d.ts +7 -15
  19. package/lib/utils/dep-aggregation.js +9 -22
  20. package/lib/utils/detect-native-packages.d.ts +83 -116
  21. package/lib/utils/detect-native-packages.js +111 -149
  22. package/lib/utils/dir-link.d.ts +29 -48
  23. package/lib/utils/dir-link.js +33 -56
  24. package/lib/utils/discover-showcases.d.ts +6 -7
  25. package/lib/utils/discover-showcases.js +11 -15
  26. package/lib/utils/dlx-cache.d.ts +29 -38
  27. package/lib/utils/dlx-cache.js +53 -63
  28. package/lib/utils/force-exit.d.ts +0 -3
  29. package/lib/utils/force-exit.js +13 -24
  30. package/lib/utils/gi-search-path.d.ts +16 -24
  31. package/lib/utils/gi-search-path.js +54 -82
  32. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  33. package/lib/utils/gjs-bundle-guard.js +35 -57
  34. package/lib/utils/gjs-source-escape.d.ts +2 -2
  35. package/lib/utils/gjs-source-escape.js +21 -39
  36. package/lib/utils/gjsify-shim.d.ts +17 -20
  37. package/lib/utils/gjsify-shim.js +80 -120
  38. package/lib/utils/html-entry.d.ts +8 -12
  39. package/lib/utils/html-entry.js +19 -28
  40. package/lib/utils/install-backend-native.d.ts +123 -161
  41. package/lib/utils/install-backend-native.js +536 -737
  42. package/lib/utils/install-backend.d.ts +42 -57
  43. package/lib/utils/install-backend.js +16 -21
  44. package/lib/utils/install-cache-fs.d.ts +10 -15
  45. package/lib/utils/install-cache-fs.js +18 -26
  46. package/lib/utils/install-lock.d.ts +5 -6
  47. package/lib/utils/install-lock.js +46 -65
  48. package/lib/utils/install-packument-cache.d.ts +27 -33
  49. package/lib/utils/install-packument-cache.js +44 -57
  50. package/lib/utils/install-progress.d.ts +2 -9
  51. package/lib/utils/install-progress.js +14 -39
  52. package/lib/utils/install-tarball-cache.d.ts +14 -20
  53. package/lib/utils/install-tarball-cache.js +41 -59
  54. package/lib/utils/library-output.d.ts +9 -22
  55. package/lib/utils/library-output.js +19 -37
  56. package/lib/utils/load-npmrc.d.ts +5 -6
  57. package/lib/utils/load-npmrc.js +18 -27
  58. package/lib/utils/node-bundle-guard.d.ts +4 -8
  59. package/lib/utils/node-bundle-guard.js +21 -35
  60. package/lib/utils/node-script.js +33 -54
  61. package/lib/utils/node-version.d.ts +5 -14
  62. package/lib/utils/node-version.js +15 -24
  63. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  64. package/lib/utils/normalize-bundler-options.js +24 -42
  65. package/lib/utils/npm-oidc.d.ts +23 -37
  66. package/lib/utils/npm-oidc.js +28 -53
  67. package/lib/utils/npm-otp-cache.d.ts +4 -4
  68. package/lib/utils/npm-otp-cache.js +20 -25
  69. package/lib/utils/npm-otp.d.ts +20 -30
  70. package/lib/utils/npm-otp.js +32 -51
  71. package/lib/utils/onboard-probe.d.ts +7 -13
  72. package/lib/utils/onboard-probe.js +16 -27
  73. package/lib/utils/parse-spec.d.ts +3 -3
  74. package/lib/utils/parse-spec.js +4 -5
  75. package/lib/utils/pkg-json-edit.d.ts +7 -12
  76. package/lib/utils/pkg-json-edit.js +13 -26
  77. package/lib/utils/pkg-json.d.ts +11 -18
  78. package/lib/utils/pkg-json.js +19 -28
  79. package/lib/utils/prefixed-output.d.ts +3 -4
  80. package/lib/utils/prefixed-output.js +13 -19
  81. package/lib/utils/prompt.d.ts +3 -4
  82. package/lib/utils/prompt.js +14 -18
  83. package/lib/utils/publish-diagnose.d.ts +11 -16
  84. package/lib/utils/publish-diagnose.js +15 -28
  85. package/lib/utils/publish-headers.d.ts +7 -11
  86. package/lib/utils/publish-headers.js +36 -65
  87. package/lib/utils/publishable-packages.d.ts +4 -6
  88. package/lib/utils/publishable-packages.js +14 -18
  89. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  90. package/lib/utils/resolve-gjs-entry.js +10 -27
  91. package/lib/utils/resolve-npm-package.d.ts +6 -11
  92. package/lib/utils/resolve-npm-package.js +29 -63
  93. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  94. package/lib/utils/resolve-plugin-by-name.js +9 -14
  95. package/lib/utils/run-gjs.d.ts +16 -21
  96. package/lib/utils/run-gjs.js +31 -56
  97. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  98. package/lib/utils/run-lifecycle-script.js +23 -42
  99. package/lib/utils/run-node.d.ts +21 -33
  100. package/lib/utils/run-node.js +43 -64
  101. package/lib/utils/runtimes.d.ts +25 -35
  102. package/lib/utils/runtimes.js +34 -46
  103. package/lib/utils/spawn.d.ts +32 -48
  104. package/lib/utils/spawn.js +51 -84
  105. package/lib/utils/stdin.d.ts +2 -2
  106. package/lib/utils/stdin.js +10 -25
  107. package/lib/utils/suggest.d.ts +6 -7
  108. package/lib/utils/suggest.js +8 -10
  109. package/lib/utils/system-gi.d.ts +18 -27
  110. package/lib/utils/system-gi.js +60 -114
  111. package/lib/utils/trust-registry.d.ts +4 -8
  112. package/lib/utils/trust-registry.js +7 -15
  113. package/lib/utils/win32-command.d.ts +8 -14
  114. package/lib/utils/win32-command.js +29 -43
  115. package/lib/utils/workspace-root.js +7 -12
  116. package/package.json +18 -18
@@ -1,24 +1,18 @@
1
1
  /**
2
2
  * Which kind of directory link this platform uses.
3
3
  *
4
- * Spelled with a NAME for the POSIX case rather than `undefined`, so both
5
- * branches can be requested explicitly. `dirLinkTarget` takes this as a
6
- * defaulted parameter, and a defaulted parameter cannot receive `undefined`:
7
- * passing it IS how you ask for the default. So while POSIX was spelled
8
- * `undefined`, `dirLinkTarget(link, target, undefined)` meant "use the host's
9
- * kind" — the POSIX branch was unexercisable on Windows, and the win32 branch
10
- * unexercisable anywhere else. The suite read as if it covered both and did
11
- * not, which is precisely the shape the header above blames for the `'dir'`
12
- * bug reaching a release.
4
+ * The POSIX case is spelled with a NAME rather than `undefined` so both branches
5
+ * stay explicitly requestable: `dirLinkTarget` takes this as a DEFAULTED
6
+ * parameter, and a defaulted parameter cannot receive `undefined` — passing it IS
7
+ * how you ask for the host's kind. Spelled `undefined`, each branch was
8
+ * unexercisable off its own platform while the suite read as if it covered both.
13
9
  */
14
10
  export type DirLinkKind = 'junction' | 'symlink';
15
11
  /** The link kind directory links use on this platform. */
16
12
  export declare const DIR_LINK_KIND: DirLinkKind;
17
13
  /**
18
- * The link type a directory link must pass to `fs.symlink`.
19
- *
20
- * `undefined` on POSIX is deliberate: Node's default is correct there, and
21
- * passing a type would be noise. Derived from {@link DIR_LINK_KIND} so the
14
+ * The link type a directory link must pass to `fs.symlink` — `undefined` on POSIX,
15
+ * where Node's default is correct. Derived from {@link DIR_LINK_KIND} so the
22
16
  * platform decision has exactly one source.
23
17
  */
24
18
  export declare const DIR_LINK_TYPE: 'junction' | undefined;
@@ -31,34 +25,26 @@ export declare const dirLinksAreJunctions: () => boolean;
31
25
  * on POSIX so the tree stays movable (gotcha 2).
32
26
  *
33
27
  * `linkKind` is a PARAMETER, defaulting to the host's, for the same reason
34
- * `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
35
- * then unit-testable from a Linux host, which is the only place anyone runs the
36
- * tests. A branch nobody can exercise is how the `'dir'` bug survived to a
37
- * release — and see {@link DirLinkKind} for why the POSIX side had to be named
38
- * rather than spelled `undefined` before that promise actually held.
28
+ * `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is then
29
+ * unit-testable from a Linux host, the only place anyone runs the tests.
39
30
  *
40
- * BOTH paths must be in the SAME canonical space — either both canonical or
41
- * both spelled through the same symlinks. The POSIX target is relative to the
42
- * link's own directory, but the kernel resolves it from the link's REAL
43
- * directory, so mixing the two spaces yields a link that points nowhere. macOS
44
- * is where this actually happens: `os.tmpdir()` is `/var/folders/…` and `/var`
45
- * is a symlink to `/private/var`, so a `linkPath` built from `tmpdir()` and an
46
- * `absTarget` that has been through `realpathSync` disagree, and the relative
47
- * path between them walks out of the real tree. `realpathSync` whichever side
48
- * is not already canonical before calling this.
31
+ * BOTH paths must be in the SAME canonical space. The POSIX target is relative to
32
+ * the link's own directory, but the kernel resolves it from the link's REAL
33
+ * directory, so mixing the two yields a link pointing nowhere. macOS is where this
34
+ * bites: `os.tmpdir()` is `/var/folders/…` and `/var` is a symlink to
35
+ * `/private/var`, so a `linkPath` from `tmpdir()` and an `absTarget` that went
36
+ * through `realpathSync` disagree and the relative path between them walks out of
37
+ * the real tree. `realpathSync` whichever side is not already canonical.
49
38
  *
50
- * @param linkPath where the link itself will live
51
39
  * @param absTarget the directory it should point at, as an ABSOLUTE path
52
40
  * @param linkKind override the host's link kind (tests only)
53
41
  */
54
42
  export declare function dirLinkTarget(linkPath: string, absTarget: string, linkKind?: DirLinkKind): string;
55
43
  /**
56
44
  * Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
57
- * spelling for the host.
58
- *
59
- * `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
60
- * describes, and it would only misbehave on Windows, so it is rejected here on
61
- * every platform rather than left to surface on the one host nobody tests on.
45
+ * spelling for the host. A relative `absTarget` is gotcha 1 and would only misbehave
46
+ * on Windows, so it is rejected on every platform rather than left to surface on the
47
+ * one host nobody tests on.
62
48
  */
63
49
  export declare function linkDirSync(linkPath: string, absTarget: string): void;
64
50
  /** Promise-returning twin of {@link linkDirSync}. */
@@ -66,21 +52,16 @@ export declare function linkDir(linkPath: string, absTarget: string): Promise<vo
66
52
  /**
67
53
  * Recreate an EXISTING link at a new path, preserving what it points at.
68
54
  *
69
- * A different job from {@link linkDirSync}, and it cannot reuse it: the source
70
- * link's target is whatever it already is — frequently relative, deliberately so
71
- * — while a junction demands an absolute one. Copying the target verbatim is
72
- * therefore right on POSIX and wrong on Windows the moment the target is a
73
- * directory.
74
- *
75
- * So the target's KIND decides: a link to a directory becomes a junction with
76
- * the target resolved against the source link's own directory (the resolution
77
- * Node will not do for you — see gotcha 1 above); anything else stays a plain
78
- * file symlink, which Windows allows unprivileged.
79
- *
80
- * A dangling source link keeps the verbatim target: it points nowhere either
81
- * way, and guessing a kind for it would turn a faithful copy into a fabrication.
55
+ * Cannot reuse {@link linkDirSync}: the source link's target is whatever it
56
+ * already is — frequently relative, deliberately so — while a junction demands an
57
+ * absolute one, so copying verbatim is right on POSIX and wrong on Windows the
58
+ * moment the target is a directory. The target's KIND therefore decides: a link to
59
+ * a directory becomes a junction with the target resolved against the source
60
+ * link's own directory (gotcha 1); anything else stays a plain file symlink, which
61
+ * Windows allows unprivileged. A dangling source link keeps the verbatim target —
62
+ * guessing a kind for it would turn a faithful copy into a fabrication.
82
63
  *
83
- * Primitive fs ops only — this runs under the GJS facade as well as Node, which
84
- * is why there is no `fs.cp`/`opendir` here.
64
+ * Primitive fs ops only: this runs under the GJS facade as well as Node, hence no
65
+ * `fs.cp`/`opendir`.
85
66
  */
86
67
  export declare function replicateLinkSync(srcLink: string, dstLink: string): void;
@@ -1,19 +1,12 @@
1
1
  // Creating a link to a DIRECTORY, portably.
2
2
  //
3
- // WHY THIS FILE EXISTS. Windows cannot create a directory *symlink* without
4
- // elevation or Developer Mode — the call fails with `EPERM` — but any user can
5
- // create an NTFS **junction**, which is why npm, yarn and pnpm all use junctions
6
- // for directory links. `commands/install.ts` learned that and encoded it, twice,
7
- // as a local constant for its workspace links. Nothing shared it, so the next
8
- // site to link a directory got it wrong: `dlx-cache.ts` linked with `'dir'` and
9
- // every non-elevated Windows user hit
10
- //
11
- // Could not resolve showcase "canvas2d-fireworks": EPERM: operation not
12
- // permitted, symlink '…\<prepare-dir>' -> '…\pkg.tmp-…'
13
- //
14
- // on a plain `npx @gjsify/cli@latest showcase …`. The knowledge existed in the
15
- // repo and the bug shipped anyway, which is the argument for a single callable
16
- // definition rather than a convention.
3
+ // Windows cannot create a directory *symlink* without elevation or Developer Mode
4
+ // (`EPERM`), but any user can create an NTFS **junction**, which is why npm, yarn
5
+ // and pnpm all use junctions for directory links. Encoded as a local constant and
6
+ // shared nowhere, the next site got it wrong: `dlx-cache.ts` linked with `'dir'` and
7
+ // every non-elevated Windows user hit `EPERM: operation not permitted, symlink` on a
8
+ // plain `npx @gjsify/cli@latest showcase …`. Hence one callable definition rather
9
+ // than a convention.
17
10
  //
18
11
  // THE TWO GOTCHAS, both load-bearing:
19
12
  //
@@ -25,19 +18,16 @@
25
18
  // tree: a relative symlink survives the whole tree being moved or mounted
26
19
  // elsewhere. So the target spelling is per-platform, not one string.
27
20
  //
28
- // Autodetection is NOT a substitute. Node picks `'file'` or `'dir'` when the
29
- // type argument is omitted — never `'junction'` — so an omitted type is the
30
- // failing case, not the safe default.
21
+ // Autodetection is no substitute on Windows: with the type argument omitted Node
22
+ // picks `'file'` or `'dir'`, never `'junction'`.
31
23
  import { readlinkSync, statSync, symlinkSync } from 'node:fs';
32
24
  import { symlink } from 'node:fs/promises';
33
25
  import { dirname, relative, resolve } from 'node:path';
34
26
  /** The link kind directory links use on this platform. */
35
27
  export const DIR_LINK_KIND = process.platform === 'win32' ? 'junction' : 'symlink';
36
28
  /**
37
- * The link type a directory link must pass to `fs.symlink`.
38
- *
39
- * `undefined` on POSIX is deliberate: Node's default is correct there, and
40
- * passing a type would be noise. Derived from {@link DIR_LINK_KIND} so the
29
+ * The link type a directory link must pass to `fs.symlink` — `undefined` on POSIX,
30
+ * where Node's default is correct. Derived from {@link DIR_LINK_KIND} so the
41
31
  * platform decision has exactly one source.
42
32
  */
43
33
  export const DIR_LINK_TYPE = DIR_LINK_KIND === 'junction' ? 'junction' : undefined;
@@ -50,23 +40,17 @@ export const dirLinksAreJunctions = () => DIR_LINK_KIND === 'junction';
50
40
  * on POSIX so the tree stays movable (gotcha 2).
51
41
  *
52
42
  * `linkKind` is a PARAMETER, defaulting to the host's, for the same reason
53
- * `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
54
- * then unit-testable from a Linux host, which is the only place anyone runs the
55
- * tests. A branch nobody can exercise is how the `'dir'` bug survived to a
56
- * release — and see {@link DirLinkKind} for why the POSIX side had to be named
57
- * rather than spelled `undefined` before that promise actually held.
43
+ * `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is then
44
+ * unit-testable from a Linux host, the only place anyone runs the tests.
58
45
  *
59
- * BOTH paths must be in the SAME canonical space — either both canonical or
60
- * both spelled through the same symlinks. The POSIX target is relative to the
61
- * link's own directory, but the kernel resolves it from the link's REAL
62
- * directory, so mixing the two spaces yields a link that points nowhere. macOS
63
- * is where this actually happens: `os.tmpdir()` is `/var/folders/…` and `/var`
64
- * is a symlink to `/private/var`, so a `linkPath` built from `tmpdir()` and an
65
- * `absTarget` that has been through `realpathSync` disagree, and the relative
66
- * path between them walks out of the real tree. `realpathSync` whichever side
67
- * is not already canonical before calling this.
46
+ * BOTH paths must be in the SAME canonical space. The POSIX target is relative to
47
+ * the link's own directory, but the kernel resolves it from the link's REAL
48
+ * directory, so mixing the two yields a link pointing nowhere. macOS is where this
49
+ * bites: `os.tmpdir()` is `/var/folders/…` and `/var` is a symlink to
50
+ * `/private/var`, so a `linkPath` from `tmpdir()` and an `absTarget` that went
51
+ * through `realpathSync` disagree and the relative path between them walks out of
52
+ * the real tree. `realpathSync` whichever side is not already canonical.
68
53
  *
69
- * @param linkPath where the link itself will live
70
54
  * @param absTarget the directory it should point at, as an ABSOLUTE path
71
55
  * @param linkKind override the host's link kind (tests only)
72
56
  */
@@ -75,11 +59,9 @@ export function dirLinkTarget(linkPath, absTarget, linkKind = DIR_LINK_KIND) {
75
59
  }
76
60
  /**
77
61
  * Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
78
- * spelling for the host.
79
- *
80
- * `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
81
- * describes, and it would only misbehave on Windows, so it is rejected here on
82
- * every platform rather than left to surface on the one host nobody tests on.
62
+ * spelling for the host. A relative `absTarget` is gotcha 1 and would only misbehave
63
+ * on Windows, so it is rejected on every platform rather than left to surface on the
64
+ * one host nobody tests on.
83
65
  */
84
66
  export function linkDirSync(linkPath, absTarget) {
85
67
  assertAbsolute(absTarget);
@@ -93,22 +75,17 @@ export async function linkDir(linkPath, absTarget) {
93
75
  /**
94
76
  * Recreate an EXISTING link at a new path, preserving what it points at.
95
77
  *
96
- * A different job from {@link linkDirSync}, and it cannot reuse it: the source
97
- * link's target is whatever it already is — frequently relative, deliberately so
98
- * — while a junction demands an absolute one. Copying the target verbatim is
99
- * therefore right on POSIX and wrong on Windows the moment the target is a
100
- * directory.
101
- *
102
- * So the target's KIND decides: a link to a directory becomes a junction with
103
- * the target resolved against the source link's own directory (the resolution
104
- * Node will not do for you — see gotcha 1 above); anything else stays a plain
105
- * file symlink, which Windows allows unprivileged.
106
- *
107
- * A dangling source link keeps the verbatim target: it points nowhere either
108
- * way, and guessing a kind for it would turn a faithful copy into a fabrication.
78
+ * Cannot reuse {@link linkDirSync}: the source link's target is whatever it
79
+ * already is — frequently relative, deliberately so — while a junction demands an
80
+ * absolute one, so copying verbatim is right on POSIX and wrong on Windows the
81
+ * moment the target is a directory. The target's KIND therefore decides: a link to
82
+ * a directory becomes a junction with the target resolved against the source
83
+ * link's own directory (gotcha 1); anything else stays a plain file symlink, which
84
+ * Windows allows unprivileged. A dangling source link keeps the verbatim target —
85
+ * guessing a kind for it would turn a faithful copy into a fabrication.
109
86
  *
110
- * Primitive fs ops only — this runs under the GJS facade as well as Node, which
111
- * is why there is no `fs.cp`/`opendir` here.
87
+ * Primitive fs ops only: this runs under the GJS facade as well as Node, hence no
88
+ * `fs.cp`/`opendir`.
112
89
  */
113
90
  export function replicateLinkSync(srcLink, dstLink) {
114
91
  const rawTarget = readlinkSync(srcLink);
@@ -1,17 +1,16 @@
1
1
  export interface ShowcaseInfo {
2
- /** Short name, e.g. "three-postprocessing-pixel" */
2
+ /** Short name, the `gjsify showcase <name>` argument. */
3
3
  name: string;
4
- /** Full npm package name, e.g. "@gjsify/example-dom-three-postprocessing-pixel" */
4
+ /** Full npm package name, e.g. "@gjsify/example-dom-three-postprocessing-pixel". */
5
5
  packageName: string;
6
- /** Category: "dom" or "node" */
6
+ /** `"dom"` or `"node"`. */
7
7
  category: string;
8
- /** Description for the list view */
9
8
  description: string;
10
9
  }
11
10
  /**
12
- * Read the curated showcase list from `showcases.json`. Returns showcases
13
- * sorted by category then name. An empty list (or missing manifest) yields
14
- * an empty array — `gjsify showcase` then prints the empty-state message.
11
+ * Read the curated showcase list from `showcases.json`, sorted by category then
12
+ * name. A missing or unparseable manifest yields `[]`, letting `gjsify showcase`
13
+ * print its empty-state message instead of failing.
15
14
  */
16
15
  export declare function discoverShowcases(): ShowcaseInfo[];
17
16
  /** Find a single showcase by short name. */
@@ -1,29 +1,25 @@
1
1
  // Static discovery of showcase packages from `showcases.json`.
2
2
  //
3
- // Earlier versions read showcases from the CLI's own `package.json#dependencies`
4
- // — every showcase had to be a direct CLI dependency. That made the CLI tarball
5
- // blow up with each new showcase and required a CLI rebuild to publish a new
6
- // one. Static manifest decouples both: the CLI reads the manifest at runtime,
7
- // `gjsify showcase <name>` delegates to `gjsify dlx <package>`.
3
+ // A manifest read at runtime, not CLI `dependencies`: a showcase must not have to
4
+ // be a direct CLI dependency, or every new one grows the CLI tarball and needs a
5
+ // CLI rebuild to publish. `gjsify showcase <name>` delegates to `gjsify dlx`.
8
6
  import { existsSync, readFileSync } from 'node:fs';
9
7
  import { dirname, join } from 'node:path';
10
8
  import { fileURLToPath } from 'node:url';
11
9
  function manifestPath() {
12
10
  const here = dirname(fileURLToPath(import.meta.url));
13
- // `showcases.json` lives at the CLI package root. Its position relative to
14
- // this module depends on how the CLI is loaded:
15
- // - `lib/utils/discover-showcases.js` (Node/Bun/Deno) → `../../showcases.json`.
16
- // - the committed `dist/cli.gjs.mjs` GJS bundle (`gjsify showcase` under
17
- // gjs) → `import.meta.url` is `dist/cli.gjs.mjs`, so the manifest is one
18
- // level up (`../showcases.json`), not two — without this the gjs CLI
19
- // finds no manifest and `showcase` wrongly reports "No showcases found".
11
+ // `showcases.json` sits at the CLI package root, but its depth below this
12
+ // module depends on how the CLI was loaded: two levels from
13
+ // `lib/utils/discover-showcases.js` (Node/Bun/Deno), one from the committed
14
+ // `dist/cli.gjs.mjs` bundle. Try both, or the gjs CLI finds no manifest and
15
+ // `showcase` wrongly reports that there are none.
20
16
  const candidates = [join(here, '..', '..', 'showcases.json'), join(here, '..', 'showcases.json')];
21
17
  return candidates.find((c) => existsSync(c)) ?? candidates[0];
22
18
  }
23
19
  /**
24
- * Read the curated showcase list from `showcases.json`. Returns showcases
25
- * sorted by category then name. An empty list (or missing manifest) yields
26
- * an empty array — `gjsify showcase` then prints the empty-state message.
20
+ * Read the curated showcase list from `showcases.json`, sorted by category then
21
+ * name. A missing or unparseable manifest yields `[]`, letting `gjsify showcase`
22
+ * print its empty-state message instead of failing.
27
23
  */
28
24
  export function discoverShowcases() {
29
25
  const path = manifestPath();
@@ -11,9 +11,8 @@ export declare function cacheDirFor(cacheKey: string): string;
11
11
  /** A fresh prepare directory under the per-key cache, named timestamp-pid. */
12
12
  export declare function makePrepareDir(cacheDir: string): string;
13
13
  /**
14
- * If <cacheDir>/pkg points to a target whose mtime + maxAge < now, return its
15
- * realpath. Returns undefined when the link doesn't exist, isn't a symlink,
16
- * has been removed, or has expired.
14
+ * The realpath of `<cacheDir>/pkg` when it is a symlink within its TTL; undefined
15
+ * when the link is missing, is not a symlink, has been removed, or has expired.
17
16
  */
18
17
  export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: number): string | undefined;
19
18
  /**
@@ -26,30 +25,25 @@ export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: numb
26
25
  * Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
27
26
  * — a parallel process won, return its realpath.
28
27
  *
29
- * WINDOWS, both halves of which shipped broken:
28
+ * WINDOWS breaks both halves:
30
29
  *
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.
30
+ * • A directory SYMLINK needs elevation or Developer Mode, so step 1 goes
31
+ * through {@link linkDirSync} for an NTFS junction — the choice npm and yarn
32
+ * make. A raw `symlinkSync(…, 'dir')` died with `EPERM: operation not
33
+ * permitted, symlink` on a plain `npx @gjsify/cli@latest showcase …`.
34
+ * • Step 2's rename-over-existing is a POSIX guarantee only: `MoveFileEx` will
35
+ * not replace an existing DIRECTORY, and a junction is a directory reparse
36
+ * point. On the second run the rename fails `EPERM`, the catch below reads it
37
+ * as "race lost", and the OLD target comes back — not a crash, but worse: the
38
+ * cache silently never refreshes. Windows therefore unlinks first and accepts
39
+ * a non-atomic window, which the race handling already covers.
45
40
  *
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.
41
+ * @param opts.junctions force the Windows link strategy (TESTS ONLY), defaulting to
42
+ * the host's. A parameter for the same reason `dirLinkTarget` takes `linkKind`:
43
+ * nothing in CI runs on Windows, so an injected `true` is the ONLY way the
44
+ * unlink-then-rename ordering is ever EXECUTED. It proves the ORDERING and that a
45
+ * second swap really refreshes; it cannot prove `MoveFileEx`/junction syscall
46
+ * behaviour.
53
47
  */
54
48
  export declare function symlinkSwap(cacheDir: string, prepareDir: string, opts?: {
55
49
  junctions?: boolean;
@@ -57,23 +51,20 @@ export declare function symlinkSwap(cacheDir: string, prepareDir: string, opts?:
57
51
  /**
58
52
  * Remove `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`.
59
53
  *
60
- * Every `gjsify dlx` miss creates a fresh prepare dir and swaps the `pkg` link
61
- * onto it; the previous one is then unreferenced and stayed on disk forever,
62
- * because this function accepted a TTL and did nothing with it. A full npm tree
63
- * per miss makes that the largest thing gjsify writes to a user's cache.
54
+ * Every `gjsify dlx` miss creates a fresh prepare dir and swaps the `pkg` link onto
55
+ * it, leaving the previous one unreferenced — a full npm tree per miss, the largest
56
+ * thing gjsify writes to a user's cache.
64
57
  *
65
58
  * Three rules keep it from deleting something it should not:
66
59
  *
67
60
  * 1. **Only names `makePrepareDir` could have produced** (`<hex>-<hex>`). The
68
- * cache dir also holds `pkg` and, briefly, `pkg.tmp-<ts>-<pid>` links from
69
- * a concurrent swap — and a name-shape guard is what makes "delete the
70
- * rest" safe against anything a future version puts here.
71
- * 2. **Never the LIVE target**, whatever its age. A long-lived cache entry
72
- * that is still current is exactly the one worth keeping, and it ages past
73
- * any TTL while being used on every run.
74
- * 3. **Never throws.** This is housekeeping on a best-effort path: a losing
75
- * race (a parallel dlx removing the same dir) or a read-only cache must not
76
- * fail the command the user actually ran.
61
+ * cache dir also holds `pkg` and, briefly, `pkg.tmp-<ts>-<pid>` links from a
62
+ * concurrent swap; the name-shape guard is what makes "delete the rest" safe
63
+ * against anything a future version puts here.
64
+ * 2. **Never the LIVE target**, whatever its age — a still-current entry ages
65
+ * past any TTL precisely because it is used on every run.
66
+ * 3. **Never throws.** Best-effort housekeeping: a lost race against a parallel
67
+ * dlx, or a read-only cache, must not fail the command the user ran.
77
68
  */
78
69
  export declare function cleanupStalePrepareDirs(cacheDir: string, maxAgeMinutes?: number): void;
79
70
  /** Resolve absolute path to the installed package's directory inside cache. */
@@ -50,9 +50,8 @@ export function makePrepareDir(cacheDir) {
50
50
  return dir;
51
51
  }
52
52
  /**
53
- * If <cacheDir>/pkg points to a target whose mtime + maxAge < now, return its
54
- * realpath. Returns undefined when the link doesn't exist, isn't a symlink,
55
- * has been removed, or has expired.
53
+ * The realpath of `<cacheDir>/pkg` when it is a symlink within its TTL; undefined
54
+ * when the link is missing, is not a symlink, has been removed, or has expired.
56
55
  */
57
56
  export function getValidCachedPkg(cacheDir, maxAgeMinutes = DEFAULT_TTL_MIN) {
58
57
  const linkPath = join(cacheDir, 'pkg');
@@ -87,52 +86,46 @@ export function getValidCachedPkg(cacheDir, maxAgeMinutes = DEFAULT_TTL_MIN) {
87
86
  * Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
88
87
  * — a parallel process won, return its realpath.
89
88
  *
90
- * WINDOWS, both halves of which shipped broken:
89
+ * WINDOWS breaks both halves:
91
90
  *
92
- * • Step 1 used `symlinkSync(…, 'dir')`, and a directory SYMLINK needs
93
- * elevation or Developer Mode there, so a plain
94
- * `npx @gjsify/cli@latest showcase …` died with `EPERM: operation not
95
- * permitted, symlink`. It now goes through {@link linkDirSync}, which picks
96
- * an NTFS junction — the same choice npm and yarn make, and the one
97
- * `commands/install.ts` already made for workspace links without sharing it.
98
- * • Step 2's rename-over-existing is a POSIX guarantee, not a Windows one:
99
- * `MoveFileEx` will not replace an existing DIRECTORY, and a junction is a
100
- * directory reparse point. So on the second run — when `pkg` already exists
101
- * — the rename failed with `EPERM`, the catch below read that as "race
102
- * lost", and the function returned the OLD target. That is not a crash but
103
- * it is worse than one: the cache silently never refreshed. Windows
104
- * therefore unlinks first and accepts the non-atomic window, which the
105
- * existing race handling already covers.
91
+ * • A directory SYMLINK needs elevation or Developer Mode, so step 1 goes
92
+ * through {@link linkDirSync} for an NTFS junction — the choice npm and yarn
93
+ * make. A raw `symlinkSync(…, 'dir')` died with `EPERM: operation not
94
+ * permitted, symlink` on a plain `npx @gjsify/cli@latest showcase …`.
95
+ * • Step 2's rename-over-existing is a POSIX guarantee only: `MoveFileEx` will
96
+ * not replace an existing DIRECTORY, and a junction is a directory reparse
97
+ * point. On the second run the rename fails `EPERM`, the catch below reads it
98
+ * as "race lost", and the OLD target comes back — not a crash, but worse: the
99
+ * cache silently never refreshes. Windows therefore unlinks first and accepts
100
+ * a non-atomic window, which the race handling already covers.
106
101
  *
107
- * @param opts.junctions force the Windows link strategy (TESTS ONLY). Defaults
108
- * to the host's. It is a parameter for the same reason `dirLinkTarget` takes
109
- * `linkType`: nothing in CI runs on Windows, so an injected `true` is the ONLY
110
- * way the unlink-then-rename ordering above is ever EXECUTED, and an
111
- * unexecutable Windows branch is exactly how the `'dir'` bug shipped. What the
112
- * injection proves off-host is the ORDERING and that the second swap really
113
- * refreshes; what it cannot prove is `MoveFileEx`/junction syscall behaviour.
102
+ * @param opts.junctions force the Windows link strategy (TESTS ONLY), defaulting to
103
+ * the host's. A parameter for the same reason `dirLinkTarget` takes `linkKind`:
104
+ * nothing in CI runs on Windows, so an injected `true` is the ONLY way the
105
+ * unlink-then-rename ordering is ever EXECUTED. It proves the ORDERING and that a
106
+ * second swap really refreshes; it cannot prove `MoveFileEx`/junction syscall
107
+ * behaviour.
114
108
  */
115
109
  export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
116
110
  const junctions = opts.junctions ?? dirLinksAreJunctions();
117
111
  const linkPath = join(cacheDir, 'pkg');
118
112
  const tmpName = `pkg.tmp-${Date.now().toString(16)}-${process.pid.toString(16)}`;
119
113
  const tmpLink = join(cacheDir, tmpName);
120
- // If we cannot even create the tmp link, give up (the error propagates).
114
+ // If the tmp link cannot be created, give up (the error propagates).
121
115
  linkDirSync(tmpLink, prepareDir);
122
116
  try {
123
117
  if (junctions) {
124
- // Windows: no rename-over-a-directory. Removing first opens a window
125
- // in which `pkg` is absent; a concurrent reader treats that as a cache
126
- // miss and prepares its own, which is correct, just wasteful. force:true
127
- // keeps a first-ever swap (nothing to remove) from throwing.
118
+ // Windows: no rename-over-a-directory. Removing first opens a window in
119
+ // which `pkg` is absent; a concurrent reader treats that as a miss and
120
+ // prepares its own — correct, just wasteful. `force:true` keeps a
121
+ // first-ever swap (nothing to remove) from throwing.
128
122
  //
129
123
  // maxRetries/retryDelay for the same reason `install-backend-native.ts`
130
- // wraps every delete in `rmWithRetry`: on Windows a delete fails with
131
- // EBUSY/EPERM while ANY process holds a handle (an antivirus scan, a
132
- // concurrent reader that just realpath'd this very link), and the
133
- // deletion then stays PENDING — so without the retry a transient
134
- // handle sends us down the "race lost" branch and the cache silently
135
- // does not refresh. Free on POSIX, where the first attempt succeeds.
124
+ // wraps deletes in `rmWithRetry`: on Windows a delete fails EBUSY/EPERM
125
+ // while ANY process holds a handle (an antivirus scan, a concurrent
126
+ // reader that just realpath'd this link) and then stays PENDING, so
127
+ // without the retry a transient handle takes the "race lost" branch and
128
+ // the cache silently does not refresh. Free on POSIX.
136
129
  rmSync(linkPath, { force: true, recursive: false, maxRetries: 10, retryDelay: 100 });
137
130
  }
138
131
  renameSync(tmpLink, linkPath);
@@ -140,21 +133,21 @@ export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
140
133
  catch (err) {
141
134
  const code = err.code;
142
135
  if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST' || code === 'EACCES') {
143
- // Race lost — clean up our tmp and use whoever won. force:true is
144
- // the non-throwing spelling of "remove if still there"; any other
145
- // failure on our own pid-unique link is real and should surface.
136
+ // Race lost — clean up our tmp and use whoever won. `force:true` is the
137
+ // non-throwing "remove if still there"; any other failure on our own
138
+ // pid-unique link is real and should surface.
146
139
  rmSync(tmpLink, { force: true });
147
140
  try {
148
141
  return realpathSync(linkPath);
149
142
  }
150
143
  catch {
151
- // Windows only, and only because the junction path unlinks before
152
- // it renames: "race lost" can also mean the winner is INSIDE that
153
- // non-atomic window, so `pkg` momentarily does not exist and this
154
- // would throw ENOENT out of a function whose caller reports it to
155
- // the user as `Could not resolve showcase "<name>"`. Our own
156
- // prepareDir is a complete, correctly installed tree — a truthful
157
- // answer for THIS run. The next run reads whichever link won.
144
+ // Windows only, because the junction path unlinks before it renames:
145
+ // "race lost" can also mean the winner is INSIDE that non-atomic
146
+ // window, so `pkg` momentarily does not exist and this would throw
147
+ // ENOENT out to a caller that reports it as `Could not resolve
148
+ // showcase "<name>"`. Our own prepareDir is a complete, correctly
149
+ // installed tree — a truthful answer for THIS run, and the next run
150
+ // reads whichever link won.
158
151
  return realpathSync(prepareDir);
159
152
  }
160
153
  }
@@ -165,28 +158,25 @@ export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
165
158
  /**
166
159
  * Remove `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`.
167
160
  *
168
- * Every `gjsify dlx` miss creates a fresh prepare dir and swaps the `pkg` link
169
- * onto it; the previous one is then unreferenced and stayed on disk forever,
170
- * because this function accepted a TTL and did nothing with it. A full npm tree
171
- * per miss makes that the largest thing gjsify writes to a user's cache.
161
+ * Every `gjsify dlx` miss creates a fresh prepare dir and swaps the `pkg` link onto
162
+ * it, leaving the previous one unreferenced — a full npm tree per miss, the largest
163
+ * thing gjsify writes to a user's cache.
172
164
  *
173
165
  * Three rules keep it from deleting something it should not:
174
166
  *
175
167
  * 1. **Only names `makePrepareDir` could have produced** (`<hex>-<hex>`). The
176
- * cache dir also holds `pkg` and, briefly, `pkg.tmp-<ts>-<pid>` links from
177
- * a concurrent swap — and a name-shape guard is what makes "delete the
178
- * rest" safe against anything a future version puts here.
179
- * 2. **Never the LIVE target**, whatever its age. A long-lived cache entry
180
- * that is still current is exactly the one worth keeping, and it ages past
181
- * any TTL while being used on every run.
182
- * 3. **Never throws.** This is housekeeping on a best-effort path: a losing
183
- * race (a parallel dlx removing the same dir) or a read-only cache must not
184
- * fail the command the user actually ran.
168
+ * cache dir also holds `pkg` and, briefly, `pkg.tmp-<ts>-<pid>` links from a
169
+ * concurrent swap; the name-shape guard is what makes "delete the rest" safe
170
+ * against anything a future version puts here.
171
+ * 2. **Never the LIVE target**, whatever its age — a still-current entry ages
172
+ * past any TTL precisely because it is used on every run.
173
+ * 3. **Never throws.** Best-effort housekeeping: a lost race against a parallel
174
+ * dlx, or a read-only cache, must not fail the command the user ran.
185
175
  */
186
176
  export function cleanupStalePrepareDirs(cacheDir, maxAgeMinutes = DEFAULT_TTL_MIN) {
187
177
  const cutoff = Date.now() - maxAgeMinutes * ONE_MINUTE_MS;
188
- // The live target, resolved once. An unreadable or absent `pkg` link simply
189
- // means nothing is live yet — every prepare dir is then a candidate.
178
+ // An unreadable or absent `pkg` link means nothing is live yet, so every prepare
179
+ // dir is a candidate.
190
180
  let live;
191
181
  try {
192
182
  live = realpathSync(join(cacheDir, 'pkg'));
@@ -213,8 +203,8 @@ export function cleanupStalePrepareDirs(cacheDir, maxAgeMinutes = DEFAULT_TTL_MI
213
203
  rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
214
204
  }
215
205
  catch {
216
- // Lost a race with another dlx, or the entry vanished between the
217
- // readdir and the stat. Both mean "someone else handled it".
206
+ // Lost a race with another dlx, or the entry vanished between readdir and
207
+ // stat. Both mean someone else handled it.
218
208
  }
219
209
  }
220
210
  }
@@ -3,8 +3,5 @@
3
3
  *
4
4
  * Safe to call from any context, including an async callback where a bare
5
5
  * `process.exit()` would be dropped under GJS. Does not return.
6
- *
7
- * `process.exitCode` is set FIRST so that a host which somehow reaches natural
8
- * shutdown anyway still reports the right code.
9
6
  */
10
7
  export declare function forceExit(code: number): never;