@gjsify/cli 0.51.1 → 0.53.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 (126) hide show
  1. package/dist/cli.gjs.mjs +539 -493
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +37 -1
  5. package/lib/bundler-pick.js +108 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/affected-classify.d.ts +7 -1
  8. package/lib/commands/affected-classify.js +66 -9
  9. package/lib/commands/affected-script-refs.d.ts +20 -0
  10. package/lib/commands/affected-script-refs.js +218 -0
  11. package/lib/commands/affected.js +24 -2
  12. package/lib/commands/env.d.ts +31 -0
  13. package/lib/commands/env.js +114 -0
  14. package/lib/commands/exec.d.ts +10 -0
  15. package/lib/commands/exec.js +208 -0
  16. package/lib/commands/flatpak/diff.js +8 -2
  17. package/lib/commands/flatpak/init.js +4 -1
  18. package/lib/commands/flatpak/sources.js +4 -3
  19. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  20. package/lib/commands/flatpak/sync-flathub.js +149 -7
  21. package/lib/commands/flatpak/utils.d.ts +10 -0
  22. package/lib/commands/flatpak/utils.js +10 -0
  23. package/lib/commands/foreach.js +5 -97
  24. package/lib/commands/format.js +26 -6
  25. package/lib/commands/index.d.ts +4 -0
  26. package/lib/commands/index.js +4 -0
  27. package/lib/commands/install.d.ts +2 -2
  28. package/lib/commands/install.js +81 -5
  29. package/lib/commands/link.d.ts +13 -0
  30. package/lib/commands/link.js +243 -0
  31. package/lib/commands/onboard.js +18 -2
  32. package/lib/commands/publish.d.ts +34 -0
  33. package/lib/commands/publish.js +153 -8
  34. package/lib/commands/self-update.js +2 -1
  35. package/lib/commands/ship.js +19 -1
  36. package/lib/commands/test.d.ts +18 -9
  37. package/lib/commands/test.js +56 -11
  38. package/lib/commands/tsc.js +4 -4
  39. package/lib/commands/upgrade.d.ts +15 -0
  40. package/lib/commands/upgrade.js +76 -2
  41. package/lib/commands/webext/index.d.ts +2 -0
  42. package/lib/commands/webext/index.js +15 -0
  43. package/lib/commands/webext/subcommands.d.ts +16 -0
  44. package/lib/commands/webext/subcommands.js +204 -0
  45. package/lib/types/config-data.d.ts +2 -2
  46. package/lib/utils/bin-shim.d.ts +8 -2
  47. package/lib/utils/bin-shim.js +6 -5
  48. package/lib/utils/bundle-inputs.d.ts +84 -0
  49. package/lib/utils/bundle-inputs.js +268 -0
  50. package/lib/utils/check-system-deps.d.ts +5 -19
  51. package/lib/utils/check-system-deps.js +31 -94
  52. package/lib/utils/dev-link.d.ts +225 -0
  53. package/lib/utils/dev-link.js +684 -0
  54. package/lib/utils/exec-bin.d.ts +137 -0
  55. package/lib/utils/exec-bin.js +321 -0
  56. package/lib/utils/gi-runtime-paths.js +3 -2
  57. package/lib/utils/gitignore.d.ts +15 -0
  58. package/lib/utils/gitignore.js +178 -0
  59. package/lib/utils/gjsify-shim.d.ts +47 -0
  60. package/lib/utils/gjsify-shim.js +97 -8
  61. package/lib/utils/hang-watchdog.d.ts +95 -0
  62. package/lib/utils/hang-watchdog.js +194 -0
  63. package/lib/utils/install-backend-native.d.ts +33 -1
  64. package/lib/utils/install-backend-native.js +184 -20
  65. package/lib/utils/install-extraneous.d.ts +90 -0
  66. package/lib/utils/install-extraneous.js +120 -0
  67. package/lib/utils/install-lock.js +4 -25
  68. package/lib/utils/install-tarball-cache.d.ts +37 -9
  69. package/lib/utils/install-tarball-cache.js +59 -14
  70. package/lib/utils/native-skip-scan.d.ts +20 -0
  71. package/lib/utils/native-skip-scan.js +343 -0
  72. package/lib/utils/node-script.js +1 -1
  73. package/lib/utils/normalize-bundler-options.js +11 -0
  74. package/lib/utils/oxc-resolve.js +42 -3
  75. package/lib/utils/process-table.d.ts +22 -0
  76. package/lib/utils/process-table.js +188 -0
  77. package/lib/utils/prune-prefix.js +1 -1
  78. package/lib/utils/publish-pins.d.ts +38 -0
  79. package/lib/utils/publish-pins.js +47 -0
  80. package/lib/utils/publish-readback.d.ts +103 -17
  81. package/lib/utils/publish-readback.js +326 -47
  82. package/lib/utils/run-gjs.d.ts +15 -1
  83. package/lib/utils/run-gjs.js +98 -13
  84. package/lib/utils/run-node.d.ts +2 -0
  85. package/lib/utils/run-node.js +2 -1
  86. package/lib/utils/runtimes.js +14 -13
  87. package/lib/utils/ship/gzip.js +1 -1
  88. package/lib/utils/ship/icns.d.ts +14 -0
  89. package/lib/utils/ship/icns.js +83 -0
  90. package/lib/utils/ship/ico.d.ts +25 -0
  91. package/lib/utils/ship/ico.js +120 -0
  92. package/lib/utils/ship/icons.d.ts +197 -0
  93. package/lib/utils/ship/icons.js +399 -0
  94. package/lib/utils/ship/launcher.js +16 -1
  95. package/lib/utils/ship/layout.d.ts +32 -0
  96. package/lib/utils/ship/layout.js +112 -38
  97. package/lib/utils/ship/msi.d.ts +1 -1
  98. package/lib/utils/ship/msi.js +30 -5
  99. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  100. package/lib/utils/ship/pe-launcher.js +125 -4
  101. package/lib/utils/ship/plan.d.ts +1 -34
  102. package/lib/utils/ship/plan.js +7 -56
  103. package/lib/utils/ship/plist.d.ts +20 -10
  104. package/lib/utils/ship/plist.js +29 -12
  105. package/lib/utils/ship/settings.js +3 -2
  106. package/lib/utils/spawn.d.ts +13 -0
  107. package/lib/utils/spawn.js +10 -5
  108. package/lib/utils/watch-loop.d.ts +15 -1
  109. package/lib/utils/watch-loop.js +9 -5
  110. package/lib/utils/webext/build.d.ts +42 -0
  111. package/lib/utils/webext/build.js +120 -0
  112. package/lib/utils/webext/config.d.ts +72 -0
  113. package/lib/utils/webext/config.js +234 -0
  114. package/lib/utils/webext/icons.d.ts +24 -0
  115. package/lib/utils/webext/icons.js +83 -0
  116. package/lib/utils/webext/launch.d.ts +32 -0
  117. package/lib/utils/webext/launch.js +77 -0
  118. package/lib/utils/webext/manifest.d.ts +53 -0
  119. package/lib/utils/webext/manifest.js +219 -0
  120. package/lib/utils/webext/pages.d.ts +26 -0
  121. package/lib/utils/webext/pages.js +61 -0
  122. package/lib/utils/webext/targets.d.ts +13 -0
  123. package/lib/utils/webext/targets.js +36 -0
  124. package/lib/utils/win32-command.d.ts +17 -0
  125. package/lib/utils/win32-command.js +21 -6
  126. package/package.json +20 -20
@@ -1,3 +1,4 @@
1
+ import { type Win32ResolveContext } from './win32-command.js';
1
2
  export type DepSeverity = 'required' | 'optional';
2
3
  export interface DepCheck {
3
4
  /** Stable key used for install command lookup, e.g. "gjs" */
@@ -15,7 +16,8 @@ export interface DepCheck {
15
16
  export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew' | 'winget' | 'unknown';
16
17
  /**
17
18
  * True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
18
- * instead of shelling out to `which`(1).
19
+ * instead of shelling out to `which`(1), honouring `PATHEXT` on win32 — see
20
+ * `findOnPath` in `win32-command.ts`, which owns the walk.
19
21
  *
20
22
  * `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
21
23
  * CI runs in ship without it (only the `which-2.x` rpm provides it), so a
@@ -31,7 +33,7 @@ export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew
31
33
  * reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
32
34
  * cannot be imported from the shipped CLI.
33
35
  */
34
- export declare function isOnPath(cmd: string): boolean;
36
+ export declare function isOnPath(cmd: string, ctx?: Win32ResolveContext): boolean;
35
37
  /**
36
38
  * The package manager whose install command we may suggest.
37
39
  *
@@ -118,7 +120,7 @@ export declare const PACKAGE_DEPS: Record<string, string[]>;
118
120
  * Run all dependency checks. Used by `gjsify check` to show full system status.
119
121
  *
120
122
  * Required deps (gjs, gtk4, libsoup3, libadwaita, gobject-introspection,
121
- * blueprint-compiler, pkg-config, meson) are always checked.
123
+ * pkg-config, meson) are always checked.
122
124
  *
123
125
  * Optional deps are checked conditionally based on which @gjsify/* packages
124
126
  * the project (resolved from cwd) actually consumes. If no project context
@@ -130,22 +132,6 @@ export declare function runAllChecks(cwd: string): DepCheck[];
130
132
  * not required). Used by `gjsify showcase` for examples that have no native deps.
131
133
  */
132
134
  export declare function runMinimalChecks(): DepCheck[];
133
- /**
134
- * Is a blueprint-compiler reachable, by the SAME rule the build uses?
135
- *
136
- * Delegates to `@gjsify/vite-plugin-blueprint/resolve` rather than probing PATH
137
- * here, because the two answers must not be able to disagree: on win32 the
138
- * compiler is normally an MSYS2 script that is deliberately NOT on PATH, so a
139
- * PATH-only check would report "missing" on a host where every `.blp` builds
140
- * fine — a check failing for the wrong reason, which is the failure mode this
141
- * file's own history is made of.
142
- *
143
- * `--version` is not run. MSYS2 ships blueprint-compiler as a shebang script
144
- * with no `.exe`, so `checkBinary` cannot execute it on Windows; presence of the
145
- * resolved pair is the honest answer, and the `source` is worth reporting
146
- * because "found via msys2" explains an install PATH does not show.
147
- */
148
- export declare function checkBlueprintCompiler(requiredBy?: string[]): DepCheck;
149
135
  /** Check gwebgl npm package (project first, CLI fallback). Optional — only needed by @gjsify/webgl users. */
150
136
  export declare function checkGwebgl(cwd: string): DepCheck;
151
137
  /**
@@ -1,5 +1,5 @@
1
1
  // System dependency checker for gjsify CLI.
2
- // Uses execFileSync with explicit argument arrays — no shell injection possible.
2
+ // Uses spawnSync with explicit argument arrays — no shell injection possible.
3
3
  // All binary names are hardcoded constants, never derived from user input.
4
4
  //
5
5
  // Severity model:
@@ -10,28 +10,31 @@
10
10
  // optional system deps are only checked if the corresponding @gjsify/* package
11
11
  // is in the project's dependency tree. A user with only @gjsify/fs in their
12
12
  // project never sees a warning about libmanette.
13
- import { execFileSync } from 'node:child_process';
13
+ import { spawnSync } from 'node:child_process';
14
14
  import { dirname, join, resolve } from 'node:path';
15
15
  import { createRequire } from 'node:module';
16
16
  import { pathToFileURL } from 'node:url';
17
17
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
18
- // The zero-dependency subpath, deliberately: importing the plugin root would
19
- // pull vite/execa/minify-xml into a system CHECK.
20
- import { resolveBlueprintCompiler } from '@gjsify/vite-plugin-blueprint/resolve';
21
18
  import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
22
19
  import { findSystemTypelib } from './gi-typelib.js';
20
+ import { toSpawnable } from './spawn.js';
21
+ import { findOnPath } from './win32-command.js';
23
22
  /** Run a binary and return its stdout trimmed, or null if it fails. */
24
23
  function tryExecFile(binary, args) {
25
- try {
26
- return execFileSync(binary, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
27
- }
28
- catch {
29
- return null;
30
- }
24
+ // `spawnSync`, not `execFileSync`: only the former takes the verbatim-argv
25
+ // flag the cmd.exe route of `toSpawnable` needs.
26
+ const inv = toSpawnable(binary, args);
27
+ const r = spawnSync(inv.cmd, inv.args, {
28
+ encoding: 'utf-8',
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ windowsVerbatimArguments: inv.windowsVerbatimArguments,
31
+ });
32
+ return r.error || r.status !== 0 ? null : r.stdout.trim();
31
33
  }
32
34
  /**
33
35
  * True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
34
- * instead of shelling out to `which`(1).
36
+ * instead of shelling out to `which`(1), honouring `PATHEXT` on win32 — see
37
+ * `findOnPath` in `win32-command.ts`, which owns the walk.
35
38
  *
36
39
  * `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
37
40
  * CI runs in ship without it (only the `which-2.x` rpm provides it), so a
@@ -47,23 +50,12 @@ function tryExecFile(binary, args) {
47
50
  * reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
48
51
  * cannot be imported from the shipped CLI.
49
52
  */
50
- export function isOnPath(cmd) {
51
- const pathVar = process.env.PATH;
52
- if (!pathVar)
53
- return false;
54
- const sep = process.platform === 'win32' ? ';' : ':';
55
- for (const dir of pathVar.split(sep)) {
56
- if (!dir)
57
- continue;
58
- try {
59
- if (existsSync(join(dir, cmd)))
60
- return true;
61
- }
62
- catch {
63
- // An inaccessible PATH entry is not an answer about `cmd`.
64
- }
65
- }
66
- return false;
53
+ export function isOnPath(cmd, ctx = hostPathContext()) {
54
+ return findOnPath(cmd, ctx) !== undefined;
55
+ }
56
+ /** This process's PATH, filesystem and platform — what {@link isOnPath} asks by default. */
57
+ function hostPathContext() {
58
+ return { platform: process.platform, env: process.env, exists: existsSync, join };
67
59
  }
68
60
  /** Check if a binary exists and optionally capture its version output. */
69
61
  function checkBinary(id, name, binary, versionArgs, severity, parseVersion, requiredBy) {
@@ -393,7 +385,7 @@ function computeNeededOptionalDeps(cwd) {
393
385
  * Run all dependency checks. Used by `gjsify check` to show full system status.
394
386
  *
395
387
  * Required deps (gjs, gtk4, libsoup3, libadwaita, gobject-introspection,
396
- * blueprint-compiler, pkg-config, meson) are always checked.
388
+ * pkg-config, meson) are always checked.
397
389
  *
398
390
  * Optional deps are checked conditionally based on which @gjsify/* packages
399
391
  * the project (resolved from cwd) actually consumes. If no project context
@@ -454,53 +446,19 @@ export function runMinimalChecks() {
454
446
  results.push(checkMsvcRuntime());
455
447
  return results;
456
448
  }
457
- /**
458
- * Is a blueprint-compiler reachable, by the SAME rule the build uses?
459
- *
460
- * Delegates to `@gjsify/vite-plugin-blueprint/resolve` rather than probing PATH
461
- * here, because the two answers must not be able to disagree: on win32 the
462
- * compiler is normally an MSYS2 script that is deliberately NOT on PATH, so a
463
- * PATH-only check would report "missing" on a host where every `.blp` builds
464
- * fine — a check failing for the wrong reason, which is the failure mode this
465
- * file's own history is made of.
466
- *
467
- * `--version` is not run. MSYS2 ships blueprint-compiler as a shebang script
468
- * with no `.exe`, so `checkBinary` cannot execute it on Windows; presence of the
469
- * resolved pair is the honest answer, and the `source` is worth reporting
470
- * because "found via msys2" explains an install PATH does not show.
471
- */
472
- export function checkBlueprintCompiler(requiredBy) {
473
- const resolved = resolveBlueprintCompiler();
474
- return {
475
- id: 'blueprint-compiler',
476
- name: 'blueprint-compiler (.blp templates)',
477
- found: resolved !== null,
478
- version: resolved ? `found via ${resolved.source}` : undefined,
479
- severity: 'optional',
480
- requiredBy,
481
- };
482
- }
483
449
  /** Check gwebgl npm package (project first, CLI fallback). Optional — only needed by @gjsify/webgl users. */
484
450
  export function checkGwebgl(cwd) {
485
451
  return checkNpmPackage('gwebgl', 'gwebgl (@gjsify/webgl)', '@gjsify/webgl', cwd, 'optional', ['@gjsify/webgl']);
486
452
  }
487
453
  /**
488
454
  * Required system dependencies — always checked, missing → exit 1.
489
- * Includes the core build toolchain (pkg-config, meson, blueprint-compiler)
490
- * and the foundational libraries (gtk4, libadwaita, libsoup3,
491
- * gobject-introspection) that nearly every gjsify app needs.
455
+ * Includes the core build toolchain (pkg-config, meson) and the foundational
456
+ * libraries (gtk4, libadwaita, libsoup3, gobject-introspection) that nearly
457
+ * every gjsify app needs.
492
458
  */
493
459
  function runRequiredChecks(_cwd) {
494
460
  const results = [];
495
- // Build toolchain. NO blueprint-compiler here: it is checked by
496
- // `checkBlueprintCompiler()` among the build toolchain below, through the
497
- // SAME resolver the build uses. A second `checkBinary` probe lived here and
498
- // was wrong twice over — it reported a PATH miss on win32, where the
499
- // compiler is normally an MSYS2 script deliberately off PATH (the exact case
500
- // that function's comment exists to explain), and it made `system-check`
501
- // exit 1 on any host without it, `.blp` files in the project or not. It also
502
- // printed a second entry under a second display name and put
503
- // `blueprint-compiler` into the install hint twice.
461
+ // Build toolchain.
504
462
  results.push(checkBinary('pkg-config', 'pkg-config', 'pkg-config', ['--version'], 'required'));
505
463
  results.push(checkBinary('meson', 'Meson', 'meson', ['--version'], 'required'));
506
464
  // Foundational libraries — by TYPELIB, the runtime question. See checkGiLibrary.
@@ -549,13 +507,12 @@ function runNativeBuildToolchainChecks() {
549
507
  // Narrower on purpose: only the three bridges that link an upstream Rust
550
508
  // crate by Cargo path-dependency need a Rust toolchain.
551
509
  const rustBridges = ['@gjsify/lightningcss-native', '@gjsify/oxfmt-native', '@gjsify/rolldown-native'];
552
- // Every showcase/app whose window comes from a `.blp` template. NOT a Vala
553
- // bridge and nothing to do with meson — it is a BUILD-time toolchain in the
554
- // same sense, and it belongs here so `gjsify system-check` names it before a
555
- // build does.
556
- const blueprintConsumers = ['@gjsify/vite-plugin-blueprint'];
510
+ // NO blueprint-compiler row, and it is a deletion rather than an omission: ADR 0053 clause 5
511
+ // moved the `.blp` transform into `@gjsify/blueprint`, so a `.blp` template asks nothing of
512
+ // this host any more. A row for it would be this command asking for a toolchain the build
513
+ // cannot use — the exact "check failing for the wrong reason" its own history is made of.
514
+ // ADR 0063 is the deletion and what the compiler is still FOR.
557
515
  return [
558
- checkBlueprintCompiler(blueprintConsumers),
559
516
  checkBinary('ninja', 'Ninja', 'ninja', ['--version'], 'optional', undefined, valaBridges),
560
517
  checkBinary('vala', 'Vala', 'valac', ['--version'], 'optional', (out) => out.replace(/^Vala\s+/i, ''), valaBridges),
561
518
  checkBinary('cargo', 'Cargo (Rust)', 'cargo', ['--version'], 'optional', (out) => out.replace(/^cargo\s+/i, '').split(' ')[0] ?? out, rustBridges),
@@ -596,7 +553,6 @@ const PM_PACKAGES = {
596
553
  apt: {
597
554
  nodejs: 'nodejs',
598
555
  gjs: 'gjs',
599
- 'blueprint-compiler': 'blueprint-compiler',
600
556
  'pkg-config': 'pkg-config',
601
557
  meson: 'meson',
602
558
  ninja: 'ninja-build',
@@ -622,7 +578,6 @@ const PM_PACKAGES = {
622
578
  dnf: {
623
579
  nodejs: 'nodejs',
624
580
  gjs: 'gjs',
625
- 'blueprint-compiler': 'blueprint-compiler',
626
581
  'pkg-config': 'pkgconf-pkg-config',
627
582
  meson: 'meson',
628
583
  ninja: 'ninja-build',
@@ -648,7 +603,6 @@ const PM_PACKAGES = {
648
603
  pacman: {
649
604
  nodejs: 'nodejs',
650
605
  gjs: 'gjs',
651
- 'blueprint-compiler': 'blueprint-compiler',
652
606
  'pkg-config': 'pkgconf',
653
607
  meson: 'meson',
654
608
  ninja: 'ninja',
@@ -674,7 +628,6 @@ const PM_PACKAGES = {
674
628
  zypper: {
675
629
  nodejs: 'nodejs',
676
630
  gjs: 'gjs',
677
- 'blueprint-compiler': 'blueprint-compiler',
678
631
  'pkg-config': 'pkg-config',
679
632
  meson: 'meson',
680
633
  ninja: 'ninja',
@@ -700,7 +653,6 @@ const PM_PACKAGES = {
700
653
  apk: {
701
654
  nodejs: 'nodejs',
702
655
  gjs: 'gjs',
703
- 'blueprint-compiler': 'blueprint-compiler',
704
656
  'pkg-config': 'pkgconf',
705
657
  meson: 'meson',
706
658
  ninja: 'ninja',
@@ -748,7 +700,6 @@ const PM_PACKAGES = {
748
700
  brew: {
749
701
  nodejs: 'node',
750
702
  gjs: 'gjs',
751
- 'blueprint-compiler': 'blueprint-compiler',
752
703
  'pkg-config': 'pkgconf',
753
704
  meson: 'meson',
754
705
  ninja: 'ninja',
@@ -804,24 +755,11 @@ export function buildInstallCommand(pm, missing) {
804
755
  const pkgMap = PM_PACKAGES[pm];
805
756
  const pkgs = [];
806
757
  const npmDeps = [];
807
- const standalone = [];
808
758
  for (const dep of missing) {
809
759
  if (dep.id === 'gwebgl') {
810
760
  npmDeps.push('@gjsify/webgl');
811
761
  continue;
812
762
  }
813
- // blueprint-compiler on Windows is the one dep whose install command is
814
- // not this host's package manager. winget has no package for it, and it
815
- // cannot get one that works: the tool needs PyGObject, which publishes
816
- // no Windows wheel, so MSYS2 — which ships Python, PyGObject and the
817
- // typelibs already fitted together — is the only source. Its command is
818
- // not `winget install`-shaped, so it cannot live in PM_PACKAGES without
819
- // producing a line that looks right and fails.
820
- if (dep.id === 'blueprint-compiler' && process.platform === 'win32') {
821
- standalone.push('pacman -S mingw-w64-ucrt-x86_64-blueprint-compiler mingw-w64-ucrt-x86_64-gtk4 ' +
822
- 'mingw-w64-ucrt-x86_64-libadwaita # inside MSYS2; found automatically afterwards');
823
- continue;
824
- }
825
763
  // No `continue` for `nodejs`. It USED to be unmissable — the row was the
826
764
  // literal `found: true` — and since it became a real probe it can report
827
765
  // absent under GJS, so skipping it here printed "Missing optional: Node.js"
@@ -842,7 +780,6 @@ export function buildInstallCommand(pm, missing) {
842
780
  if (npmDeps.length > 0) {
843
781
  lines.push(`npm install ${npmDeps.join(' ')}`);
844
782
  }
845
- lines.push(...standalone);
846
783
  return lines.length > 0 ? lines.join('\n ') : null;
847
784
  }
848
785
  /**
@@ -0,0 +1,225 @@
1
+ import { type Workspace } from '@gjsify/workspace';
2
+ /** The consumer-local override file. Git-ignored; never committed. */
3
+ export declare const DEV_LINK_FILE = ".gjsify-link.json";
4
+ /**
5
+ * What `.gjsify-link.json` holds.
6
+ *
7
+ * A POINTER plus a selector, never a materialised list of package names: `link`
8
+ * and `install` then derive the same set from the same two inputs, so a checkout
9
+ * that gains or loses a workspace package cannot leave the two disagreeing about
10
+ * what is linked.
11
+ */
12
+ export interface DevLinkOverride {
13
+ /** Format version — an unknown one is refused rather than guessed at. */
14
+ version: 1;
15
+ /** The gjsify checkout to link from. Absolute, as written by `gjsify link`. */
16
+ checkout: string;
17
+ /**
18
+ * Name globs selecting which of its workspace packages take part. EMPTY means
19
+ * every one of them — deliberately not `["*"]`, which would select NOTHING:
20
+ * measured, `@gjsify/workspace`'s glob dialect maps `*` to `[^/]*`, so a bare
21
+ * star never matches a SCOPED name. The spelling for a whole scope is
22
+ * `@gjsify/*`, the same dialect `gjsify foreach --include` takes.
23
+ */
24
+ packages: string[];
25
+ }
26
+ /** One `node_modules/<name>` → checkout package directory link. */
27
+ export interface DevLink {
28
+ name: string;
29
+ /** Absolute path of the workspace package in the checkout. */
30
+ target: string;
31
+ /** Absolute path of the link inside the consumer's `node_modules`. */
32
+ linkPath: string;
33
+ }
34
+ /** An override that has been read, validated and turned into a link plan. */
35
+ export interface ActiveDevLinks {
36
+ /** Absolute path of the override file, for every message that names it. */
37
+ overridePath: string;
38
+ /** The consumer root the override belongs to. */
39
+ consumerRoot: string;
40
+ /** The validated checkout it points at. */
41
+ checkout: string;
42
+ links: DevLink[];
43
+ /** Just the names — what the installer excludes from its fetch set. */
44
+ names: Set<string>;
45
+ }
46
+ export declare function devLinkPath(root: string): string;
47
+ /**
48
+ * Read the override at `root`, or `null` when there is none.
49
+ *
50
+ * A file that exists but does not parse is an ERROR, not a `null`: this decides
51
+ * where a build's inputs come from, and "unreadable" must never be answered with
52
+ * the registry's copy while the developer believes their checkout is in play.
53
+ */
54
+ export declare function readDevLinkOverride(root: string): DevLinkOverride | null;
55
+ export declare function writeDevLinkOverride(root: string, override: DevLinkOverride): string;
56
+ /** Remove the override file; returns its path when one was there. */
57
+ export declare function removeDevLinkOverride(root: string): string | null;
58
+ /**
59
+ * Which directory's override governs an install started in `startDir`.
60
+ *
61
+ * Same walk `projectInstallNative` does for the install itself: a member of a
62
+ * workspace installs the workspace, so the override that applies is the
63
+ * workspace root's — otherwise `gjsify install` inside a member would silently
64
+ * ignore a link the root declares.
65
+ */
66
+ export declare function devLinkRoot(startDir: string): string;
67
+ /**
68
+ * Turn `override.checkout` into the workspace list behind it, or refuse.
69
+ *
70
+ * A DEAD LINK IS AN ERROR WITH A PATH, never a quiet fall back to the registry:
71
+ * the developer asked for their checkout, and installing the published copy
72
+ * instead produces a build that looks like the one they wanted and is not. Both
73
+ * refusals print the measured path, because "it points somewhere wrong" is only
74
+ * actionable with the somewhere in it.
75
+ */
76
+ export declare function resolveCheckoutWorkspaces(checkout: string, overridePath: string): Workspace[];
77
+ /**
78
+ * The dependency names the consumer would otherwise take from the registry.
79
+ *
80
+ * Two readers, one answer: what its manifests DECLARE (so a fresh clone can be
81
+ * linked before it has ever installed) and what its `node_modules` already HOLDS
82
+ * (so transitive `@gjsify/*` packages — the ones nobody declares and every build
83
+ * loads — are covered too).
84
+ */
85
+ export declare function consumerKnownNames(consumerRoot: string): Set<string>;
86
+ /**
87
+ * The links an override asks for: the checkout's workspace packages, narrowed by
88
+ * the name globs, INTERSECTED with what the consumer actually depends on.
89
+ *
90
+ * The intersection is the point. Linking every workspace package of a 200-member
91
+ * monorepo into a consumer that uses eight would put 200 directories it never
92
+ * declared into its tree, and leave `gjsify unlink` with no honest answer about
93
+ * what to restore. Pure, so every selection rule is decidable without a
94
+ * filesystem.
95
+ */
96
+ export declare function planDevLinks(opts: {
97
+ consumerRoot: string;
98
+ workspaces: readonly Workspace[];
99
+ patterns: readonly string[];
100
+ knownNames: ReadonlySet<string>;
101
+ }): DevLink[];
102
+ /**
103
+ * Read + validate the override at `consumerRoot` and plan its links, or `null`
104
+ * when no override is there. Throws for every other shape — see
105
+ * {@link resolveCheckoutWorkspaces}.
106
+ */
107
+ export declare function prepareDevLinks(consumerRoot: string): ActiveDevLinks | null;
108
+ /**
109
+ * (Re)create every planned link. Idempotent — a link already pointing at the
110
+ * right target is left alone, so a re-run costs no unlink/relink race against a
111
+ * process reading through it.
112
+ *
113
+ * Returns the links it actually wrote.
114
+ */
115
+ export declare function applyDevLinks(links: readonly DevLink[]): DevLink[];
116
+ /**
117
+ * Remove the links an override produced.
118
+ *
119
+ * ONLY a symlink whose target is inside the checkout is removed. A real
120
+ * directory at that path is the registry copy — some other tool's, or an install
121
+ * that ran with the override already gone — and deleting it would turn `unlink`
122
+ * into a destructive command for a tree it did not create. Same asymmetry
123
+ * `install-extraneous.ts` reasons from: a wrong deletion loses work, a wrong
124
+ * refusal costs one message.
125
+ */
126
+ export declare function removeDevLinks(links: readonly DevLink[]): DevLink[];
127
+ /**
128
+ * Every `node_modules/<name>` symlink that resolves INSIDE one of `checkouts`.
129
+ *
130
+ * The PLAN is not a census. `removeDevLinks` only ever visits the links the
131
+ * CURRENT override asks for, so a link the previous override made and the new one
132
+ * dropped is invisible to it — measured: link everything, relink with
133
+ * `--packages 'is-odd'`, and `node_modules/is-number` stays pointing into the
134
+ * checkout while nothing in the plan names it. From there `assertNodeModulesDest`
135
+ * aborted EVERY later `gjsify install`, and `gjsify unlink` could not help because
136
+ * it removed the override (the escape route) before hitting the same abort. Only a
137
+ * hand-written `rm` got the project back.
138
+ *
139
+ * So the ground truth for "what is linked" is the tree, not the file. Reads one
140
+ * level of `node_modules` plus one level into each `@scope/`, which is where
141
+ * top-level links live; `.bin` and real directories are skipped by construction.
142
+ */
143
+ export declare function scanDevLinks(consumerRoot: string, checkouts: readonly string[]): DevLink[];
144
+ /**
145
+ * Remove every dev link into `checkouts` that `keep` does not name.
146
+ *
147
+ * What makes re-linking with a narrower `--packages` a RE-selection rather than an
148
+ * addition, and what makes `unlink` leave the tree as it found it. Pass an empty
149
+ * `keep` to retire all of them.
150
+ */
151
+ export declare function retireDevLinks(consumerRoot: string, checkouts: readonly string[], keep: ReadonlySet<string>): DevLink[];
152
+ /** A linked package that cannot be imported, and why not. */
153
+ export interface UnbuiltDevLink {
154
+ link: DevLink;
155
+ reason: string;
156
+ }
157
+ /**
158
+ * Which of these links point at something that is not built.
159
+ *
160
+ * Candidates are the string entry points a manifest can carry — `main`, `module`
161
+ * and the string leaves under `exports["."]`. ANY of them existing is enough: a
162
+ * package legitimately declares entries it does not ship on every platform, and a
163
+ * refusal that fires on a healthy tree is a refusal people learn to route around.
164
+ * An entry-LESS manifest is fine, because nothing was promised that could be
165
+ * missing.
166
+ *
167
+ * An UNREADABLE manifest is NOT fine, and that is a flip from the first draft,
168
+ * which answered "built" for it. "I could not tell" is not "yes" in a check whose
169
+ * whole job is to stop a silently unusable tree.
170
+ */
171
+ export declare function unbuiltDevLinks(links: readonly DevLink[]): UnbuiltDevLink[];
172
+ /** The refusal text for {@link unbuiltDevLinks}, naming every path and the build. */
173
+ export declare function formatUnbuiltDevLinks(unbuilt: readonly UnbuiltDevLink[]): string;
174
+ /**
175
+ * FAIL CLOSED on a link to something unbuilt.
176
+ *
177
+ * `dist/`/`lib/` are git-ignored in a gjsify checkout, so "the entry point is not
178
+ * there right now" is the ordinary consequence of a branch switch, not an exotic
179
+ * state. Measured: with the entry file deleted, `gjsify install` re-linked and
180
+ * printed only "development link ACTIVE"; the consumer then died on
181
+ * MODULE_NOT_FOUND with a message naming the consumer, not the checkout.
182
+ */
183
+ export declare function assertDevLinksBuilt(links: readonly DevLink[]): void;
184
+ /**
185
+ * Make `.gjsify-link.json` invisible to git WITHOUT editing a tracked file.
186
+ *
187
+ * `info/exclude` is git's own per-clone ignore list: it is not committed and
188
+ * not shared between clones, which is exactly the lifetime of the thing it
189
+ * hides. Writing to `.gitignore` instead would put a developer's local override
190
+ * into everyone else's diff — the manifest edit this whole feature exists to
191
+ * avoid, one file over. A WORKTREE is the case to get right: the file is
192
+ * written under the repository's COMMON dir, because `info/exclude` is shared
193
+ * across a repository's worktrees and the worktree's own gitdir is not read
194
+ * (see {@link resolveGitDir}).
195
+ *
196
+ * Returns what it did, so the caller can say so. A consumer that is not a git
197
+ * repository is not an error: nothing can commit the file there either. A
198
+ * consumer whose `.git` POINTER cannot be followed IS worth a word, though —
199
+ * there the file stays visible to `git status` and one `git add -A` commits it.
200
+ */
201
+ export declare function ensureLocallyIgnored(consumerRoot: string): 'added' | 'already-ignored' | 'no-git' | 'unreadable-git';
202
+ /**
203
+ * Undo {@link ensureLocallyIgnored} — the other half of `unlink`.
204
+ *
205
+ * Measured: the `info/exclude` block outlived `gjsify unlink` (idempotently,
206
+ * so it never grew — it just never left). A command that promises to undo itself
207
+ * and leaves a line behind is not an undo, and the leftover is a rule about a file
208
+ * that no longer exists, which the next reader has to research to delete.
209
+ *
210
+ * ONLY the two lines this module wrote, matched as a pair: a `.gitignore` entry or
211
+ * a hand-written exclude line is someone else's decision, and removing it would be
212
+ * the same overreach `removeDevLinks` refuses.
213
+ */
214
+ export declare function removeLocalIgnore(consumerRoot: string): 'removed' | 'absent' | 'no-git' | 'unreadable-git';
215
+ /**
216
+ * `--immutable` must never accept an active override — FAIL CLOSED.
217
+ *
218
+ * CI, a Flatpak build and a release all run `--immutable`, and all three exist to
219
+ * build exactly what is committed. An override is a developer's local tree, so
220
+ * consuming one here would produce an artifact whose inputs are not in any
221
+ * commit and whose build reported nothing unusual. Refusing names the file,
222
+ * because the fix is to delete it and the message is the only place its path
223
+ * appears.
224
+ */
225
+ export declare function assertNoDevLinkUnderImmutable(startDir: string): void;