@gjsify/cli 0.51.1 → 0.52.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 (61) hide show
  1. package/dist/cli.gjs.mjs +505 -490
  2. package/lib/cli-app.js +3 -1
  3. package/lib/commands/affected-classify.d.ts +7 -1
  4. package/lib/commands/affected-classify.js +66 -9
  5. package/lib/commands/affected-script-refs.d.ts +20 -0
  6. package/lib/commands/affected-script-refs.js +218 -0
  7. package/lib/commands/affected.js +24 -2
  8. package/lib/commands/flatpak/diff.js +8 -2
  9. package/lib/commands/flatpak/init.js +4 -1
  10. package/lib/commands/flatpak/sources.js +4 -3
  11. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  12. package/lib/commands/flatpak/sync-flathub.js +149 -7
  13. package/lib/commands/flatpak/utils.d.ts +10 -0
  14. package/lib/commands/flatpak/utils.js +10 -0
  15. package/lib/commands/index.d.ts +1 -0
  16. package/lib/commands/index.js +1 -0
  17. package/lib/commands/install.js +76 -0
  18. package/lib/commands/link.d.ts +13 -0
  19. package/lib/commands/link.js +243 -0
  20. package/lib/commands/onboard.js +18 -2
  21. package/lib/commands/publish.d.ts +34 -0
  22. package/lib/commands/publish.js +153 -8
  23. package/lib/commands/ship.js +19 -1
  24. package/lib/types/config-data.d.ts +1 -1
  25. package/lib/utils/check-system-deps.d.ts +1 -17
  26. package/lib/utils/check-system-deps.js +10 -68
  27. package/lib/utils/dev-link.d.ts +222 -0
  28. package/lib/utils/dev-link.js +632 -0
  29. package/lib/utils/hang-watchdog.d.ts +95 -0
  30. package/lib/utils/hang-watchdog.js +194 -0
  31. package/lib/utils/install-backend-native.d.ts +14 -0
  32. package/lib/utils/install-backend-native.js +91 -13
  33. package/lib/utils/install-extraneous.d.ts +90 -0
  34. package/lib/utils/install-extraneous.js +120 -0
  35. package/lib/utils/install-tarball-cache.d.ts +37 -9
  36. package/lib/utils/install-tarball-cache.js +59 -14
  37. package/lib/utils/normalize-bundler-options.js +11 -0
  38. package/lib/utils/publish-pins.d.ts +38 -0
  39. package/lib/utils/publish-pins.js +47 -0
  40. package/lib/utils/publish-readback.d.ts +103 -17
  41. package/lib/utils/publish-readback.js +326 -47
  42. package/lib/utils/run-gjs.js +82 -6
  43. package/lib/utils/ship/icns.d.ts +14 -0
  44. package/lib/utils/ship/icns.js +83 -0
  45. package/lib/utils/ship/ico.d.ts +25 -0
  46. package/lib/utils/ship/ico.js +120 -0
  47. package/lib/utils/ship/icons.d.ts +191 -0
  48. package/lib/utils/ship/icons.js +398 -0
  49. package/lib/utils/ship/launcher.js +16 -1
  50. package/lib/utils/ship/layout.d.ts +32 -0
  51. package/lib/utils/ship/layout.js +112 -38
  52. package/lib/utils/ship/msi.d.ts +1 -1
  53. package/lib/utils/ship/msi.js +30 -5
  54. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  55. package/lib/utils/ship/pe-launcher.js +125 -4
  56. package/lib/utils/ship/plan.d.ts +1 -34
  57. package/lib/utils/ship/plan.js +7 -56
  58. package/lib/utils/ship/plist.d.ts +20 -10
  59. package/lib/utils/ship/plist.js +29 -12
  60. package/lib/utils/ship/settings.js +3 -2
  61. package/package.json +20 -20
@@ -11,7 +11,8 @@
11
11
  // 3. Resolve commit SHA via `git rev-list -n 1 <version>`
12
12
  // 4. Clone (or update) the flathub tracking-repo into $XDG_CACHE_HOME
13
13
  // 5. Surgically edit the manifest: modules[].sources[].{tag,commit}
14
- // plus x-checker-data block (inject if missing)
14
+ // plus x-checker-data block (inject if missing), and carry over the
15
+ // offline source list (see `offline sources` below)
15
16
  // 6. Create a branch, commit the changes
16
17
  // 7. (unless --no-pr) push + open a PR via `gh pr create`
17
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -21,7 +22,7 @@ import { execFile } from 'node:child_process';
21
22
  import { describeExit, spawnToCompletion } from '../../utils/spawn.js';
22
23
  import { promisify } from 'node:util';
23
24
  import { Config } from '../../config.js';
24
- import { readPackageJson } from './utils.js';
25
+ import { FLATPAK_SOURCES_FILE, readPackageJson } from './utils.js';
25
26
  const execFileAsync = promisify(execFile);
26
27
  export const flatpakSyncFlathubCommand = {
27
28
  command: 'sync-flathub',
@@ -51,6 +52,12 @@ export const flatpakSyncFlathubCommand = {
51
52
  .option('branch', {
52
53
  description: 'Branch name in the flathub-repo. Default: `update-to-<version>`.',
53
54
  type: 'string',
55
+ })
56
+ .option('sources-file', {
57
+ description: 'Generated offline tarball list to carry into the Flathub repo and name in the manifest. ' +
58
+ `Default: \`${FLATPAK_SOURCES_FILE}\`. Skipped when the tag does not carry it.`,
59
+ type: 'string',
60
+ default: FLATPAK_SOURCES_FILE,
54
61
  })
55
62
  .option('source-index', {
56
63
  description: 'Index into modules[0].sources[] to update (when manifest has multiple sources). Default: first `type: git` source.',
@@ -105,13 +112,33 @@ export const flatpakSyncFlathubCommand = {
105
112
  if (!existsSync(manifestPath)) {
106
113
  throw new Error(`[gjsify flatpak sync-flathub] Flathub manifest not found at ${manifestPath} — wrong appId / wrong flathub-repo?`);
107
114
  }
115
+ // Read AT THE TAG, not from the working tree: the manifest is about to pin
116
+ // that tag, and a list taken from an edited checkout would describe a tree
117
+ // nobody can get. Absent is the ordinary case for an app that installs
118
+ // online or vendors its dependencies, so it is a skip and not an error.
119
+ const sourcesFile = args.sourcesFile ?? FLATPAK_SOURCES_FILE;
120
+ const offline = await readSourceListAtTag(cwd, version, sourcesFile, args.verbose);
121
+ let offlineCopied = false;
122
+ if (offline !== null) {
123
+ const dest = join(cloneDir, sourcesFile);
124
+ offlineCopied = !existsSync(dest) || readFileSync(dest, 'utf-8') !== offline;
125
+ if (offlineCopied) {
126
+ writeFileSync(dest, offline, 'utf-8');
127
+ console.log(`[gjsify flatpak sync-flathub] ${sourcesFile} copied from ${version}`);
128
+ }
129
+ }
108
130
  const original = readFileSync(manifestPath, 'utf-8');
109
131
  const updated = editManifest(original, {
110
132
  tag: version,
111
133
  commit: commitSha,
112
134
  sourceIndex: args.sourceIndex,
135
+ sourcesFile: offline !== null ? sourcesFile : undefined,
113
136
  });
114
- if (updated === original) {
137
+ // NOT `updated === original` alone: the tarball list changes whenever a
138
+ // dependency does, so a release can need a PR with the pin unmoved. That
139
+ // test used to be the only one, and it would have returned "nothing to do"
140
+ // over a Flathub repo carrying a stale list.
141
+ if (updated === original && !offlineCopied) {
115
142
  console.log(`[gjsify flatpak sync-flathub] manifest already at ${version} — nothing to do.`);
116
143
  return;
117
144
  }
@@ -130,6 +157,88 @@ export const flatpakSyncFlathubCommand = {
130
157
  },
131
158
  };
132
159
  // ─── Internal helpers ────────────────────────────────────────────────────
160
+ /**
161
+ * The generated source list as of `tag`, or null when the tag does not carry one.
162
+ *
163
+ * `git show <tag>:<path>` rather than reading the working tree, so the file that
164
+ * travels to Flathub is the one the pinned commit actually contains.
165
+ *
166
+ * WHAT COMES BACK IS CHECKED, not just whether the command succeeded. A missing
167
+ * path makes real `git show` exit non-zero, so the obvious version of this
168
+ * function looks complete — and it treats an EMPTY answer as "the file exists
169
+ * and is empty", which then writes an empty file into the Flathub repo and
170
+ * names it in the manifest. That is strictly worse than skipping: the build
171
+ * still cannot install, and now the manifest says it can. Caught by the
172
+ * sync-flathub E2E, whose git stub answers every unknown subcommand with exit 0
173
+ * and no output.
174
+ *
175
+ * A blank answer is a skip. Content that is not a non-empty array of sources is
176
+ * an ERROR, because this file has exactly one producer (`gjsify flatpak
177
+ * sources`) and anything else means something upstream is broken.
178
+ */
179
+ async function readSourceListAtTag(cwd, tag, path, verbose) {
180
+ let raw;
181
+ try {
182
+ const { stdout } = await execFileAsync('git', ['show', `${tag}:${path}`], { cwd, maxBuffer: 64 * 1024 * 1024 });
183
+ raw = stdout;
184
+ }
185
+ catch (error) {
186
+ await assertGitMeantAbsent(cwd, tag, path, error);
187
+ if (verbose)
188
+ console.log(`[gjsify flatpak sync-flathub] ${path} not present at ${tag} — skipping`);
189
+ return null;
190
+ }
191
+ if (raw.trim() === '') {
192
+ if (verbose)
193
+ console.log(`[gjsify flatpak sync-flathub] ${path} is empty at ${tag} — skipping`);
194
+ return null;
195
+ }
196
+ let parsed;
197
+ try {
198
+ parsed = JSON.parse(raw);
199
+ }
200
+ catch (error) {
201
+ throw new Error(`[gjsify flatpak sync-flathub] ${path} at ${tag} is not JSON: ` +
202
+ (error instanceof Error ? error.message : String(error)));
203
+ }
204
+ if (!Array.isArray(parsed) || parsed.length === 0) {
205
+ throw new Error(`[gjsify flatpak sync-flathub] ${path} at ${tag} is not a non-empty array of flatpak sources`);
206
+ }
207
+ return raw;
208
+ }
209
+ /**
210
+ * Throw unless a failed `git show <tag>:<path>` really did mean "that tag does
211
+ * not carry that file".
212
+ *
213
+ * MEASURED, and it is why the failure cannot be read on its own: path absent,
214
+ * unknown tag and not-a-git-repository are ALL exit 128, and git translates the
215
+ * message (`Pfad … existiert nicht in …` on a German host), so neither the
216
+ * status nor the text separates them. Taken for absence, a tag nobody fetched
217
+ * moves the pin and leaves the Flathub repo's old list in place — the failure
218
+ * this file exists to prevent, now silent. `--commit <sha>` is what puts it in
219
+ * reach: it is the one path on which nothing else resolves the tag.
220
+ *
221
+ * The tag is therefore asked about separately, and a SHA is believed rather
222
+ * than an exit code — the same lesson as the read above, since a `git` that
223
+ * answers 0 with nothing must not pass for a tag that exists.
224
+ */
225
+ async function assertGitMeantAbsent(cwd, tag, path, error) {
226
+ const code = error?.code;
227
+ if (typeof code !== 'number') {
228
+ // Not git's verdict at all: ENOENT is no `git` on PATH, and
229
+ // ERR_CHILD_PROCESS_STDIO_MAXBUFFER a list past the cap set above.
230
+ throw new Error(`[gjsify flatpak sync-flathub] could not run \`git show ${tag}:${path}\` — ` +
231
+ (error instanceof Error ? error.message : String(error)));
232
+ }
233
+ // `--quiet` exits 1 rather than printing for a name it cannot resolve.
234
+ const sha = await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `${tag}^{commit}`], { cwd })
235
+ .then(({ stdout }) => stdout.trim())
236
+ .catch(() => '');
237
+ if (!sha) {
238
+ throw new Error(`[gjsify flatpak sync-flathub] ${tag} does not resolve in ${cwd}, so whether it carries ${path} is unanswered. ` +
239
+ 'Run `git fetch --tags`, pass --version <an existing tag>, or run from the app checkout.');
240
+ }
241
+ }
133
242
  async function resolveLatestTag(cwd, verbose) {
134
243
  try {
135
244
  const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0'], { cwd });
@@ -258,14 +367,17 @@ export function editManifest(original, args) {
258
367
  if (sources.length === 0) {
259
368
  throw new Error('[gjsify flatpak sync-flathub] modules[0] has no sources');
260
369
  }
261
- let idx = args.sourceIndex ?? sources.findIndex((s) => s.type === 'git');
370
+ const isObject = (s) => typeof s === 'object';
371
+ let idx = args.sourceIndex ?? sources.findIndex((s) => isObject(s) && s.type === 'git');
262
372
  if (idx < 0 || idx >= sources.length) {
263
373
  throw new Error(`[gjsify flatpak sync-flathub] no git source found in modules[0].sources (use --source-index <n>)`);
264
374
  }
265
- const source = sources[idx];
266
- if (source.type !== 'git') {
267
- throw new Error(`[gjsify flatpak sync-flathub] modules[0].sources[${idx}].type is "${source.type}", expected "git"`);
375
+ const entry = sources[idx];
376
+ if (!isObject(entry) || entry.type !== 'git') {
377
+ const what = isObject(entry) ? `type is "${entry.type}"` : `is the file reference "${entry}"`;
378
+ throw new Error(`[gjsify flatpak sync-flathub] modules[0].sources[${idx}] ${what}, expected a git source`);
268
379
  }
380
+ const source = entry;
269
381
  source.tag = args.tag;
270
382
  source.commit = args.commit;
271
383
  if (!source['x-checker-data']) {
@@ -275,12 +387,42 @@ export function editManifest(original, args) {
275
387
  'version-scheme': 'semantic',
276
388
  };
277
389
  }
390
+ // THE OFFLINE TARBALL LIST, which is the half of a Flathub bump that is easy
391
+ // to forget because nothing points at it. Flathub builds with the network
392
+ // unshared, so an app whose install reads a generated source list cannot
393
+ // build from the git checkout alone; the list has to live in the Flathub repo
394
+ // and be named here. Repointing tag and commit alone produces a manifest that
395
+ // looks correct and cannot build, and the error it eventually gives is
396
+ // whatever the app's build system says when its install fails.
397
+ //
398
+ // Measured: Learn6502 0.8.0 was the first release after its vendored
399
+ // dependency cache was dropped, and its Flathub build died on exactly this.
400
+ if (args.sourcesFile && !namesFile(sources, args.sourcesFile)) {
401
+ sources.push(args.sourcesFile);
402
+ }
278
403
  // Detect the original indent (2 vs 4 spaces) by inspecting the second
279
404
  // line — Flathub manifests are all 2-space in practice, but some
280
405
  // older ones might be 4. Preserve original convention.
281
406
  const indent = detectIndent(original);
282
407
  return JSON.stringify(manifest, null, indent) + (original.endsWith('\n') ? '\n' : '');
283
408
  }
409
+ /**
410
+ * Is `file` already among the module's bare-string sources?
411
+ *
412
+ * Compared with `./` stripped, because flatpak-builder resolves a string source
413
+ * against the manifest's own directory: `./gjsify-sources.json` and
414
+ * `gjsify-sources.json` are one file. Compared as written, a manifest holding
415
+ * the dotted spelling grows a second reference and the same tarballs are
416
+ * spliced in twice — the duplicate `dest`/`dest-filename` pairs `gjsify flatpak
417
+ * sources` dedupes its own output to avoid.
418
+ */
419
+ function namesFile(sources, file) {
420
+ const want = stripDotSlash(file);
421
+ return sources.some((entry) => typeof entry === 'string' && stripDotSlash(entry) === want);
422
+ }
423
+ function stripDotSlash(ref) {
424
+ return ref.replace(/^\.\//, '');
425
+ }
284
426
  function detectIndent(json) {
285
427
  const match = json.match(/^\{\n( +)/);
286
428
  if (match)
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The generated offline tarball list, by the name both halves have to agree on.
3
+ *
4
+ * `gjsify flatpak sources` WRITES it; `gjsify flatpak sync-flathub` carries it
5
+ * into the Flathub repo and names it in the manifest. Each spelled the default
6
+ * itself, so changing one would have left the other reading a file nobody
7
+ * writes — and that shows up as a Flathub build which cannot install, four
8
+ * steps from its cause.
9
+ */
10
+ export declare const FLATPAK_SOURCES_FILE = "gjsify-sources.json";
1
11
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
2
12
  export declare function readPackageJson(dir: string): Record<string, unknown>;
3
13
  /** True if a name string looks like a reverse-DNS Flatpak app id. */
@@ -5,6 +5,16 @@
5
5
  // header. What is left here is what only these subcommands ask.
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
+ /**
9
+ * The generated offline tarball list, by the name both halves have to agree on.
10
+ *
11
+ * `gjsify flatpak sources` WRITES it; `gjsify flatpak sync-flathub` carries it
12
+ * into the Flathub repo and names it in the manifest. Each spelled the default
13
+ * itself, so changing one would have left the other reading a file nobody
14
+ * writes — and that shows up as a Flathub build which cannot install, four
15
+ * steps from its cause.
16
+ */
17
+ export const FLATPAK_SOURCES_FILE = 'gjsify-sources.json';
8
18
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
9
19
  export function readPackageJson(dir) {
10
20
  const path = resolve(dir, 'package.json');
@@ -15,6 +15,7 @@ export { flatpakCommand } from './flatpak/index.js';
15
15
  export * from './ship.js';
16
16
  export * from './dlx.js';
17
17
  export * from './install.js';
18
+ export * from './link.js';
18
19
  export * from './foreach.js';
19
20
  export * from './workspace.js';
20
21
  export * from './pack.js';
@@ -15,6 +15,7 @@ export { flatpakCommand } from './flatpak/index.js';
15
15
  export * from './ship.js';
16
16
  export * from './dlx.js';
17
17
  export * from './install.js';
18
+ export * from './link.js';
18
19
  export * from './foreach.js';
19
20
  export * from './workspace.js';
20
21
  export * from './pack.js';
@@ -35,6 +35,7 @@ import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEn
35
35
  import { resolveHostPlatform } from '../utils/platform-check.js';
36
36
  import { pruneAfterInstall } from '../utils/prune-prefix.js';
37
37
  import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
38
+ import { applyDevLinks, assertNoDevLinkUnderImmutable, devLinkRoot, prepareDevLinks, } from '../utils/dev-link.js';
38
39
  /**
39
40
  * Link type for the workspace↔workspace directory links.
40
41
  *
@@ -157,6 +158,12 @@ export const installCommand = {
157
158
  '(--immutable forbids rewriting the lockfile). Drop one.');
158
159
  return process.exit(1);
159
160
  }
161
+ // FAIL CLOSED before anything is resolved or written: an active `gjsify
162
+ // link` override would make a local development tree an input to the one
163
+ // install shape whose whole promise is "exactly what is committed" — CI,
164
+ // the Flatpak build, a release. Throws rather than exits so `index.ts`
165
+ // prints the message that names the override file (utils/dev-link.ts).
166
+ assertNoDevLinkUnderImmutable(process.cwd());
160
167
  }
161
168
  if (args.global) {
162
169
  if (!args.packages || args.packages.length === 0) {
@@ -426,6 +433,61 @@ function depKindFromArgs(args) {
426
433
  return 'optionalDependencies';
427
434
  return 'dependencies';
428
435
  }
436
+ /**
437
+ * Wire the `gjsify link` development override for this prefix, and SAY SO.
438
+ *
439
+ * Called before the fetch phase for the same reason `wireWorkspaceSymlinks` is:
440
+ * these links point at local source and depend on no download, so writing them
441
+ * first means an install that dies mid-way still leaves a resolvable tree. Called
442
+ * again afterwards as a near no-op safety net.
443
+ *
444
+ * The announcement is not decoration. A linked tree changes what a build compiles
445
+ * without changing any file the consumer commits, so the one place it can be seen
446
+ * is the output of the command that re-applies it — this is exactly what a bare
447
+ * `npm link` does not do, and why the next `npm install` silently undoes it.
448
+ *
449
+ * Returns `null` when no override is present; throws when one names a checkout
450
+ * that is gone, is not a gjsify workspace, or is not BUILT (utils/dev-link.ts).
451
+ *
452
+ * `devLinkRoot` and not `prefix` verbatim: `assertNoDevLinkUnderImmutable` already
453
+ * walks up to the workspace root to decide whether an override is in play, and two
454
+ * answers to "which override governs this install" is one answer too many. Nothing
455
+ * rescued the difference either — the bare `install` path happens to delegate to
456
+ * the workspace root before reaching here, `install <pkg>` does not.
457
+ */
458
+ function activateDevLinks(prefix, announce) {
459
+ const active = prepareDevLinks(devLinkRoot(prefix));
460
+ if (!active)
461
+ return null;
462
+ const written = applyDevLinks(active.links);
463
+ if (!announce)
464
+ return active;
465
+ console.log(`gjsify install: development link ACTIVE (${active.overridePath}) — ${active.links.length} ` +
466
+ `package(s) come from ${active.checkout}, linked rather than fetched ` +
467
+ `(${written.length} (re)written, ${active.links.length - written.length} already current):`);
468
+ const names = active.links.map((l) => l.name);
469
+ const shown = names.slice(0, 12);
470
+ console.log(` ${shown.join(', ')}${names.length > shown.length ? `, +${names.length - shown.length} more` : ''}`);
471
+ console.log(' `gjsify unlink` restores the registry copies; `gjsify install --immutable` refuses while it exists.');
472
+ return active;
473
+ }
474
+ /**
475
+ * Re-apply the dev links after the install, and SAY SO when it changed anything.
476
+ *
477
+ * `applyDevLinks` returns what it had to (re)write, and both post-install call
478
+ * sites used to throw that away — so an installer that clobbered a link and got it
479
+ * put back looked exactly like one that never touched it. The safety net is
480
+ * supposed to be a no-op; the run where it is not is the one worth a line.
481
+ */
482
+ function reportRewiredDevLinks(active) {
483
+ if (!active)
484
+ return;
485
+ const rewired = applyDevLinks(active.links);
486
+ if (rewired.length === 0)
487
+ return;
488
+ console.log(`gjsify install: re-applied ${rewired.length} development link(s) the install had replaced: ` +
489
+ `${rewired.map((l) => l.name).join(', ')}`);
490
+ }
429
491
  async function projectInstallNative(args, signal) {
430
492
  const cwd = process.cwd();
431
493
  const pkgPath = join(cwd, 'package.json');
@@ -497,6 +559,7 @@ async function projectInstallNative(args, signal) {
497
559
  // re-acquires the same lock re-entrantly.
498
560
  const lock = await acquireInstallLock(cwd, { signal });
499
561
  try {
562
+ const devLinks = activateDevLinks(cwd, true);
500
563
  // Typed as NativeInstallOptions so the native-backend-only `optionalSpecs`
501
564
  // is CHECKED here instead of smuggled past an excess-property check;
502
565
  // `installPackages` forwards the object to that backend unchanged.
@@ -518,8 +581,14 @@ async function projectInstallNative(args, signal) {
518
581
  ...optionalDependencyNames(pkg ? [pkg] : []),
519
582
  ...(args['save-optional'] ? (args.packages ?? []).map((s) => parseSpec(s).name) : []),
520
583
  ]),
584
+ linkedNames: devLinks?.names,
521
585
  };
522
586
  const result = await installPackages(nativeOpts);
587
+ // The link survives the install — the point of reading the override at all.
588
+ // The return value is REPORTED, not dropped: a non-empty one means the
589
+ // install disturbed a link that the pre-fetch pass had already wired, which
590
+ // is a fact about the installer, not a routine no-op.
591
+ reportRewiredDevLinks(devLinks);
523
592
  // Only the `gjsify install <pkg>...` add-a-dep flow mutates the manifest;
524
593
  // the no-args refresh must not.
525
594
  if (args.packages && args.packages.length > 0 && pkg) {
@@ -650,6 +719,10 @@ async function workspaceInstallLocked(cwd, args, signal) {
650
719
  }
651
720
  }
652
721
  console.log(`gjsify install: ${workspaces.length} workspace(s), ${externalSpecs.size} external dep spec(s), ${symlinks.length} workspace symlink(s)`);
722
+ // A `gjsify link` override at the workspace ROOT governs the whole install, and
723
+ // is wired here for the same reason the shims and workspace symlinks are: before
724
+ // anything can fail on the network.
725
+ const devLinks = activateDevLinks(cwd, true);
653
726
  // EARLY, before the download/extract phase: the shims derive from workspace
654
727
  // manifests alone, while the heavy phase that follows can die mid-way (network,
655
728
  // timeout, crash). Writing them up front means a failed install can never leave
@@ -836,6 +909,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
836
909
  // `types-dev/<lib>` workspace that also exists on npm and is pinned
837
910
  // transitively in the lockfile).
838
911
  workspaceNames: new Set(byName.keys()),
912
+ linkedNames: devLinks?.names,
839
913
  specOrigins: new Map([...specOrigins].map(([k, v]) => [k, [...v]])),
840
914
  // Aggregated over EVERY member, because the specs are: a name any member
841
915
  // declares as a plain dependency stays required.
@@ -865,6 +939,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
865
939
  frozen: args.immutable,
866
940
  signal,
867
941
  workspaceNames: new Set(byName.keys()),
942
+ linkedNames: devLinks?.names,
868
943
  // Same aggregated answer as the root install: these specs are a SUBSET of
869
944
  // the root's, so classifying them differently would make one dep optional
870
945
  // at the root and required in a member.
@@ -874,6 +949,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
874
949
  }
875
950
  // A near no-op safety net in case a later step disturbed a link.
876
951
  await wireWorkspaceSymlinks();
952
+ reportRewiredDevLinks(devLinks);
877
953
  // Now that the tree is materialised, refresh the shims' GJS preamble with the
878
954
  // native prebuild dirs that only became discoverable after the install (on a
879
955
  // fresh checkout `detectNativePackages` finds nothing before extraction).
@@ -0,0 +1,13 @@
1
+ import type { LeafCommand } from '../types/index.js';
2
+ interface LinkOptions {
3
+ checkout: string;
4
+ packages?: string[];
5
+ 'dry-run'?: boolean;
6
+ }
7
+ interface UnlinkOptions {
8
+ 'dry-run'?: boolean;
9
+ install?: boolean;
10
+ }
11
+ export declare const linkCommand: LeafCommand<unknown, LinkOptions>;
12
+ export declare const unlinkCommand: LeafCommand<unknown, UnlinkOptions>;
13
+ export {};