@detergent-software/atk 9.2.0-dev.5 → 9.2.0-dev.7

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 (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/build/command-manifest.json +3 -3
  3. package/build/commands/browse.d.ts.map +1 -1
  4. package/build/commands/browse.js +119 -16
  5. package/build/commands/browse.js.map +1 -1
  6. package/build/commands/install.d.ts.map +1 -1
  7. package/build/commands/install.js +182 -147
  8. package/build/commands/install.js.map +1 -1
  9. package/build/commands/uninstall.d.ts.map +1 -1
  10. package/build/commands/uninstall.js +5 -3
  11. package/build/commands/uninstall.js.map +1 -1
  12. package/build/components/BrowseList.d.ts +3 -1
  13. package/build/components/BrowseList.d.ts.map +1 -1
  14. package/build/components/BrowseList.js +12 -6
  15. package/build/components/BrowseList.js.map +1 -1
  16. package/build/components/HelpBar.d.ts +5 -1
  17. package/build/components/HelpBar.d.ts.map +1 -1
  18. package/build/components/HelpBar.js +18 -4
  19. package/build/components/HelpBar.js.map +1 -1
  20. package/build/components/InstallConfirmBar.d.ts +4 -2
  21. package/build/components/InstallConfirmBar.d.ts.map +1 -1
  22. package/build/components/InstallConfirmBar.js +15 -2
  23. package/build/components/InstallConfirmBar.js.map +1 -1
  24. package/build/hooks/useBrowseState.d.ts +31 -1
  25. package/build/hooks/useBrowseState.d.ts.map +1 -1
  26. package/build/hooks/useBrowseState.js +99 -15
  27. package/build/hooks/useBrowseState.js.map +1 -1
  28. package/build/hooks/useUninstallState.d.ts.map +1 -1
  29. package/build/hooks/useUninstallState.js +23 -15
  30. package/build/hooks/useUninstallState.js.map +1 -1
  31. package/build/lib/format.d.ts +5 -0
  32. package/build/lib/format.d.ts.map +1 -1
  33. package/build/lib/format.js +7 -0
  34. package/build/lib/format.js.map +1 -1
  35. package/build/lib/install-targets.d.ts +90 -0
  36. package/build/lib/install-targets.d.ts.map +1 -0
  37. package/build/lib/install-targets.js +146 -0
  38. package/build/lib/install-targets.js.map +1 -0
  39. package/build/lib/installer.js +3 -2
  40. package/build/lib/installer.js.map +1 -1
  41. package/build/lib/resolver.d.ts +35 -0
  42. package/build/lib/resolver.d.ts.map +1 -1
  43. package/build/lib/resolver.js +65 -21
  44. package/build/lib/resolver.js.map +1 -1
  45. package/build/lib/uninstaller.d.ts +29 -1
  46. package/build/lib/uninstaller.d.ts.map +1 -1
  47. package/build/lib/uninstaller.js +63 -4
  48. package/build/lib/uninstaller.js.map +1 -1
  49. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp } from 'ink';
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { z } from 'zod';
5
5
  import { CrossScopeWarning, DryRunBanner, ErrorView, InstallSummary, MultiSelectList, Spinner, } from '../components/index.js';
6
6
  import { useCommand } from '../hooks/useCommand.js';
@@ -10,14 +10,16 @@ import { resolveBundleVersion } from '../lib/bundle-version.js';
10
10
  import { loadConfig } from '../lib/config.js';
11
11
  import { crossScopeNotice, findCrossScopeInstalls } from '../lib/cross-scope.js';
12
12
  import { errorGuidance, errorMessage } from '../lib/errors.js';
13
+ import { bundleMembers, installPlannedTargets, isBundleInPlace, planTargetsInstall } from '../lib/install-targets.js';
13
14
  import { isInstalledForTool } from '../lib/installed-for-tool.js';
15
+ import { InstallerError } from '../lib/installer.js';
14
16
  import { addAssetToLockfile, findInstalledAsset, identityKey, readLockfile, upgradeToDirectInstall, withLockfileErrorHandling, writeLockfile, } from '../lib/lockfile.js';
15
17
  import { executeInstallPlanForTools, withoutKeptDependencies } from '../lib/multi-tool-install.js';
16
18
  import { addNotice } from '../lib/notices.js';
17
19
  import { formatOrgName, isBareOrgScope, parseBundleSpec, resolveActiveOrg } from '../lib/org.js';
18
20
  import { renderJson, useNonInteractive } from '../lib/output.js';
19
21
  import { argument, option } from '../lib/pastel/index.js';
20
- import { fetchRegistry, findAsset, findAssetsByName, findBundle, findBundleMember, getAssetsByOrg, isDeprecated, isLatestAlias, } from '../lib/registry.js';
22
+ import { fetchRegistry, findAsset, findAssetsByName, findBundle, getAssetsByOrg, isDeprecated, isLatestAlias, } from '../lib/registry.js';
21
23
  import { resolveBundleInstallPlan, resolveInstallPlan, resolveMultipleInstallPlans, ResolverError, } from '../lib/resolver.js';
22
24
  import { scopeLabel } from '../lib/scope.js';
23
25
  import { resolvePlacingTools } from '../lib/tool-resolver.js';
@@ -36,7 +38,7 @@ export const options = z.object({
36
38
  .default(false)
37
39
  .describe(option({
38
40
  alias: 'a',
39
- description: 'Install every asset from an org scope without prompting (for `atk install @org`; required with --json or no terminal)',
41
+ description: 'Install every asset from an org scope without prompting (for `atk install @org`; required with --json, no terminal, or other names)',
40
42
  })),
41
43
  dryRun: z
42
44
  .boolean()
@@ -119,7 +121,7 @@ export const args = z
119
121
  })),
120
122
  ])
121
123
  .rest(z.string().describe(argument({
122
- description: 'More assets to install with the first, as one plan (name@version and @org/name)',
124
+ description: 'More assets, bundles or org scopes to install with the first, as one plan (name@version, @org/name, @org with --all)',
123
125
  name: 'targets',
124
126
  })));
125
127
  export default function Install({ args: [rawName, ...more], options: { adopt, all, dryRun, force, json, org, pin, projectRoot, refresh, tool, user, yes }, }) {
@@ -128,7 +130,7 @@ export default function Install({ args: [rawName, ...more], options: { adopt, al
128
130
  return _jsx(Sync, { options: { dryRun, force, json, projectRoot, refresh, tool, user } });
129
131
  }
130
132
  if (more.length > 0) {
131
- return (_jsx(InstallAssets, { adopt: adopt, dryRun: dryRun, force: force, json: json, org: org, pin: pin, projectRoot: projectRoot, specs: [rawName, ...more], tool: tool, user: user }));
133
+ return (_jsx(InstallAssets, { adopt: adopt, all: all || yes, dryRun: dryRun, force: force, json: json, org: org, pin: pin, projectRoot: projectRoot, specs: [rawName, ...more], tool: tool, user: user }));
132
134
  }
133
135
  if (isBareOrgScope(rawName)) {
134
136
  const orgName = rawName.slice(1);
@@ -149,20 +151,6 @@ async function bundleAtRequestedVersion(entry, requested, progress) {
149
151
  }
150
152
  return resolution.bundle;
151
153
  }
152
- /**
153
- * The bundle's members that one of `tools` places, as the registry has them. A member none of them
154
- * places is left out, since this run can't adopt it, and so is a ref the registry can't find, which
155
- * the install reports.
156
- */
157
- function bundleMembers(registry, bundle, tools) {
158
- return (bundle.assets ?? []).flatMap((ref) => {
159
- const found = findBundleMember(registry, bundle, ref);
160
- if (!found || !tools.some((t) => placesAsset(t.adapter, found.asset.type, found.versionData.tools)))
161
- return [];
162
- const { name, org, type } = found.asset;
163
- return [{ name, ...(org ? { org } : {}), type }];
164
- });
165
- }
166
154
  /**
167
155
  * What a bundle install says about its members outside the bundle (`kept`) and the members it
168
156
  * `adopted`. A member with no origin can be adopted; another bundle's member stays with that bundle.
@@ -273,16 +261,8 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
273
261
  // through to a misleading "asset not found".
274
262
  if (!indexBundle && (registry.bundles ?? []).some((b) => b.name === name)) {
275
263
  const assetExists = findAsset(registry, name, undefined, undefined, org);
276
- if (!assetExists) {
277
- if (inlineOrg) {
278
- throw new Error(`Bundle '${name}' not found in org '${inlineOrg}'.`);
279
- }
280
- const orgScopes = (registry.bundles ?? [])
281
- .filter((b) => b.name === name && b.org)
282
- .map((b) => `@${b.org}/${name}`);
283
- throw new Error(`Bundle '${name}' not found in the registry.` +
284
- (orgScopes.length > 0 ? ` Did you mean ${orgScopes.join(' or ')}?` : ''));
285
- }
264
+ if (!assetExists)
265
+ throw otherScopeBundleError(registry, name, inlineOrg);
286
266
  }
287
267
  // With no active org, `info` opens the only org asset of a bare name, but install doesn't
288
268
  // switch scopes on its own: it names the spec that installs that asset.
@@ -360,26 +340,7 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
360
340
  };
361
341
  // Bundle installation flow — check if already installed for ALL tools
362
342
  if (!force) {
363
- const bundleAssets = bundle.assets ?? [];
364
- // When no configured tool takes any member, fall through so the installer says why.
365
- const allInstalledForAllTools = tools.every(({ adapter: toolAdapter }) => members.length > 0 &&
366
- bundleAssets.every((ref) => {
367
- const found = findBundleMember(registry, bundle, ref);
368
- if (!found)
369
- return false;
370
- // A tool that doesn't take this member (its type, or a hook for other tools) has
371
- // nothing to check.
372
- if (!placesAsset(toolAdapter, found.asset.type, found.versionData.tools))
373
- return true;
374
- const installed = findInstalledAsset(before, ref.name, ref.type, found.asset.org);
375
- if (!installed)
376
- return false;
377
- if (requiredBundleVersion && !isFromBundleVersion(installed, bundle, requiredBundleVersion)) {
378
- return false;
379
- }
380
- return isInstalledForTool(installed, toolAdapter, projectDir);
381
- }));
382
- if (allInstalledForAllTools) {
343
+ if (isBundleInPlace(registry, bundle, before, { members, projectDir, requiredBundleVersion, tools })) {
383
344
  // Adopting a bundle that is in place only records its origin on the members with none:
384
345
  // their files, versions and pins stay as they are. Another bundle's member stays its.
385
346
  const outside = membersOutsideBundle(before, members, bundle);
@@ -521,13 +482,15 @@ function InstallAsset({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoo
521
482
  return (_jsxs(Box, { flexDirection: 'column', children: [data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
522
483
  }
523
484
  /**
524
- * `atk install a b@1.2.0 @org/c`: the named assets resolved together (shared dependencies once,
525
- * one version per asset) and installed as one plan, so a failure rolls the whole run back, as an
526
- * `@org` install does. Every name is checked before anything is written. A name already in place
527
- * for every tool is left out of the plan, and one installed only as a dependency becomes direct
528
- * once the install succeeds.
485
+ * `atk install a b@1.2.0 @org/c my-bundle @org --all`: the named assets, the bundles' members and
486
+ * the orgs' assets resolved together (shared dependencies once, one version per asset) and
487
+ * installed as one plan, so a failure rolls the whole run back, as an `@org` install does. Every
488
+ * name is checked before anything is written. An asset already in place for every tool is left out
489
+ * of the plan, and one installed only as a dependency becomes direct once the install succeeds. A
490
+ * bundle is left out when all of it is in place, as when it is installed alone; its members get
491
+ * its origin unless they are named themselves ({@link resolveTargetsInstallPlan}).
529
492
  */
530
- function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRoot, specs, tool, user, }) {
493
+ function InstallAssets({ adopt, all, dryRun, force, json, org: cliOrg, pin, projectRoot, specs, tool, user, }) {
531
494
  const { json: effectiveJson } = useNonInteractive(json);
532
495
  const { fields, resolve, resolved } = useResolvedScope({ projectRoot, user });
533
496
  const state = useCommand(async (progress) => {
@@ -535,11 +498,13 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
535
498
  if (adopt) {
536
499
  throw new Error('--adopt records a bundle as the owner of its members, so it works with one bundle only.');
537
500
  }
538
- const orgScope = specs.find(isBareOrgScope);
539
- if (orgScope) {
540
- throw new ResolverError(`${orgScope} is an org scope, which installs on its own.`, {
541
- command: installCommandFor(orgScope, flags),
542
- });
501
+ // No picker opens halfway through a run, so an org among names takes all its assets, and
502
+ // only when asked to. A dry run shows them all, as `atk install @org --dry-run` does.
503
+ const orgScopes = [...new Set(specs.filter(isBareOrgScope))];
504
+ if (orgScopes.length > 0 && !all && !dryRun) {
505
+ throw new InstallerError(`${orgScopes.join(', ')} among other names installs every asset in ` +
506
+ `${orgScopes.length === 1 ? 'it' : 'them'}, so it needs --all (-a). ` +
507
+ `To choose ${orgScopes.length === 1 ? 'its' : 'their'} assets, install ${orgScopes.length === 1 ? 'it' : 'each'} alone.`, { code: 'selection_required', command: installCommandFor(`${specs.join(' ')} --all`, flags) });
543
508
  }
544
509
  const { root: projectDir, scope: installScope } = await resolve();
545
510
  progress('Loading configuration...');
@@ -549,33 +514,65 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
549
514
  progress('Fetching registry...');
550
515
  const registry = await fetchRegistry({ force: true });
551
516
  const currentLockfile = await readLockfile(projectDir);
552
- // Every name is checked before anything is resolved or written.
553
- // Each bundle as typed, and the spec that installs it (another scope's, when only that has it).
554
- const bundles = [];
517
+ // Every name is checked before anything is resolved or written. The targets keep the order
518
+ // they were named in, which decides the origin of a member two bundles share.
555
519
  const deprecated = [];
556
- const requests = [];
557
- for (const spec of specs) {
520
+ const emptyOrgs = [];
521
+ const orgs = [];
522
+ const targets = [];
523
+ const deprecationOf = (name, deprecation) => {
524
+ if (deprecation.deprecated && !force) {
525
+ deprecated.push(`"${name}" (${deprecation.reason}${deprecation.replacement ? `; use ${deprecation.replacement}` : ''})`);
526
+ }
527
+ };
528
+ for (const spec of new Set(specs)) {
529
+ if (isBareOrgScope(spec)) {
530
+ const orgName = spec.slice(1);
531
+ const orgAssets = getAssetsByOrg(registry, orgName);
532
+ if (orgAssets.length === 0)
533
+ emptyOrgs.push(spec);
534
+ orgs.push({ count: orgAssets.length, org: orgName });
535
+ for (const { name, type } of orgAssets) {
536
+ targets.push({
537
+ fromOrg: true,
538
+ kind: 'asset',
539
+ name,
540
+ org: orgName,
541
+ spec: formatOrgName(orgName, name),
542
+ type,
543
+ });
544
+ }
545
+ continue;
546
+ }
558
547
  const { name: parsedName, version } = parseNameVersion(spec);
559
548
  const { name: nameFromOrg, org: inlineOrg } = parseBundleSpec(parsedName);
560
549
  const name = inlineOrg ? nameFromOrg : parsedName;
561
550
  const org = resolveActiveOrg(inlineOrg ?? cliOrg, currentLockfile.org, config.org);
562
- const asset = findAsset(registry, name, undefined, undefined, org)?.asset;
563
- const bundle = findBundle(registry, name, inlineOrg) ??
564
- (asset ? undefined : (registry.bundles ?? []).find((b) => b.name === name));
565
- if (bundle) {
566
- bundles.push({ install: formatOrgName(bundle.org, bundle.name) + (version ? `@${version}` : ''), spec });
551
+ const indexBundle = findBundle(registry, name, inlineOrg);
552
+ if (indexBundle) {
553
+ const bundle = await bundleAtRequestedVersion(indexBundle, version, progress);
554
+ const earlier = targets.find((t) => t.kind === 'bundle' && t.bundle.name === name && t.bundle.org === indexBundle.org);
555
+ if (earlier) {
556
+ // A bundle installs at one version, as an asset does.
557
+ if (earlier.bundle.version !== bundle.version) {
558
+ throw new ResolverError(`Bundle "${formatOrgName(bundle.org, name)}" is named at two versions: ${earlier.bundle.version} ` +
559
+ `(${earlier.spec}) and ${bundle.version} (${spec}). A bundle installs at one version.`);
560
+ }
561
+ continue;
562
+ }
563
+ deprecationOf(name, isDeprecated(registry, name, undefined, indexBundle.org));
564
+ targets.push({ bundle, kind: 'bundle', spec, ...(version ? { version } : {}) });
567
565
  continue;
568
566
  }
569
- const deprecation = isDeprecated(registry, name, asset?.type, asset ? asset.org : inlineOrg);
570
- if (deprecation.deprecated && !force) {
571
- deprecated.push(`"${name}" (${deprecation.reason}${deprecation.replacement ? `; use ${deprecation.replacement}` : ''})`);
567
+ const asset = findAsset(registry, name, undefined, undefined, org)?.asset;
568
+ if (!asset && (registry.bundles ?? []).some((b) => b.name === name)) {
569
+ throw otherScopeBundleError(registry, name, inlineOrg);
572
570
  }
573
- requests.push({ name, ...(org ? { org } : {}), spec, ...(version ? { version } : {}) });
571
+ deprecationOf(name, isDeprecated(registry, name, asset?.type, asset ? asset.org : inlineOrg));
572
+ targets.push({ kind: 'asset', name, ...(org ? { org } : {}), spec, ...(version ? { version } : {}) });
574
573
  }
575
- if (bundles.length > 0) {
576
- throw new ResolverError(bundles.length === 1
577
- ? `${bundles[0].spec} is a bundle, which installs on its own.`
578
- : `${bundles.map((b) => b.spec).join(', ')} are bundles, which install on their own.`, { command: installCommandFor(bundles[0].install, flags) });
574
+ if (emptyOrgs.length > 0) {
575
+ throw new ResolverError(`No assets found for ${emptyOrgs.join(', ')}. Nothing was installed.`);
579
576
  }
580
577
  if (deprecated.length > 0) {
581
578
  throw new ResolverError(`Deprecated: ${deprecated.join(', ')}. Nothing was installed.`, {
@@ -583,54 +580,61 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
583
580
  });
584
581
  }
585
582
  // A name every targeted tool already has in place is left out of the plan, as it is for one name.
586
- const assets = [];
587
- const upgrades = [];
588
- const toInstall = [];
589
- for (const request of requests) {
590
- const match = findAsset(registry, request.name, undefined, request.version, request.org);
591
- const existing = match
592
- ? findInstalledAsset(currentLockfile, request.name, match.asset.type, match.asset.org)
593
- : undefined;
594
- const supportingTools = existing && match ? tools.filter((t) => placesAsset(t.adapter, existing.type, match.versionData.tools)) : [];
595
- const inPlace = !force &&
596
- existing !== undefined &&
597
- supportingTools.length > 0 &&
598
- supportingTools.every((t) => isInstalledForTool(existing, t.adapter, projectDir)) &&
599
- (!request.version || request.version === existing.version);
600
- if (!inPlace) {
601
- toInstall.push(request);
602
- continue;
603
- }
604
- if (assets.some((a) => identityKey(a) === identityKey(existing)))
605
- continue;
606
- const dependency = (existing.installReason ?? 'direct') === 'dependency';
607
- if (dependency)
608
- upgrades.push(existing);
609
- assets.push({
610
- name: existing.name,
611
- ...(existing.org ? { org: existing.org } : {}),
612
- outcome: dependency ? 'upgraded-to-direct' : 'already-installed',
613
- type: existing.type,
614
- version: existing.version,
615
- });
616
- }
583
+ const adoptCommandFor = ({ bundle, version }) => installCommandFor(formatOrgName(bundle.org, bundle.name) + (version ? `@${version}` : ''), {
584
+ adopt: true,
585
+ force: false,
586
+ pin: false,
587
+ projectRoot,
588
+ tool,
589
+ user,
590
+ });
617
591
  progress('Resolving dependencies...');
618
- const plan = resolveMultipleInstallPlans(registry, toInstall.map(({ name, org, version }) => ({ name, ...(org ? { org } : {}), ...(version ? { version } : {}) })));
619
- const installed = plan.entries
620
- .filter((entry) => entry.reason === 'direct')
621
- .map((entry) => ({
622
- name: entry.asset.name,
623
- ...(entry.asset.org ? { org: entry.asset.org } : {}),
624
- outcome: 'installed',
625
- type: entry.asset.type,
626
- version: entry.version,
627
- }));
628
- const report = [...installed, ...assets];
592
+ const targetsPlan = planTargetsInstall(registry, targets, currentLockfile, tools, { force, projectDir });
593
+ const { assets: report, plan, toInstall } = targetsPlan;
594
+ const bundles = targetsPlan.bundlesInPlace.map((request) => {
595
+ const { bundle } = request;
596
+ return {
597
+ name: bundle.name,
598
+ ...(bundle.org ? { org: bundle.org } : {}),
599
+ outcome: 'already-installed',
600
+ version: bundle.version,
601
+ ...bundleReport(membersOutsideBundle(currentLockfile, bundleMembers(registry, bundle, tools), bundle), [], {
602
+ adoptCommand: adoptCommandFor(request),
603
+ bundle,
604
+ }),
605
+ };
606
+ });
629
607
  const crossScope = (resolvedPlan, installedAt) => crossScopeOf(resolvedPlan, { installedAt, json: effectiveJson, scope: installScope, tools: toolNames });
608
+ // Each bundle in the plan, in the order named, then the ones left out; and the orgs.
609
+ const targetsField = (lockfile, planned) => {
610
+ const installedBundles = toInstall.flatMap((request) => {
611
+ if (request.kind !== 'bundle')
612
+ return [];
613
+ const { bundle } = request;
614
+ const members = bundleMembers(registry, bundle, tools);
615
+ // A dry run writes nothing, so the members it leaves outside the bundle are read from the plan.
616
+ const kept = [
617
+ ...membersOutsideBundle(lockfile, members, bundle),
618
+ ...(planned ? plannedOutsideBundle(plan, lockfile, members, bundle) : []),
619
+ ];
620
+ return [
621
+ {
622
+ name: bundle.name,
623
+ ...(bundle.org ? { org: bundle.org } : {}),
624
+ outcome: 'installed',
625
+ version: bundle.version,
626
+ ...bundleReport(kept, [], { adoptCommand: adoptCommandFor(request), bundle }),
627
+ },
628
+ ];
629
+ });
630
+ const named = [...installedBundles, ...bundles];
631
+ return { ...(named.length > 0 ? { bundles: named } : {}), ...(orgs.length > 0 ? { orgs } : {}) };
632
+ };
630
633
  // A dry run says so even when every name is in place, and writes nothing (not even an upgrade).
631
634
  if (dryRun) {
632
635
  return {
633
636
  assets: report,
637
+ ...targetsField(currentLockfile, true),
634
638
  outcome: 'dry-run-install',
635
639
  plan: withoutKeptDependencies(plan, currentLockfile, tools, {
636
640
  onProgress: progress,
@@ -642,28 +646,26 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
642
646
  };
643
647
  }
644
648
  return await withLockfileErrorHandling(projectDir, async () => {
645
- const toolResults = plan.entries.length > 0
646
- ? await executeInstallPlanForTools(plan, tools, {
647
- force,
648
- onProgress: progress,
649
- onTool: (toolName) => progress(`Installing for ${toolName}...`),
650
- pin,
651
- projectDir,
652
- scope: installScope,
653
- })
654
- : [];
655
- // After the install, so a failed one leaves even the install reasons as they were.
656
- if (upgrades.length > 0) {
657
- let lockfile = await readLockfile(projectDir);
658
- for (const { name, org, type } of upgrades)
659
- lockfile = upgradeToDirectInstall(lockfile, name, type, org);
660
- await writeLockfile(projectDir, lockfile);
649
+ const toolResults = await installPlannedTargets(targetsPlan, tools, {
650
+ force,
651
+ onProgress: progress,
652
+ onTool: (toolName) => progress(`Installing for ${toolName}...`),
653
+ pin,
654
+ projectDir,
655
+ scope: installScope,
656
+ });
657
+ if (plan.entries.length === 0) {
658
+ return { assets: report, ...targetsField(currentLockfile, false), outcome: 'already-installed' };
661
659
  }
662
- if (plan.entries.length === 0)
663
- return { assets: report, outcome: 'already-installed' };
664
- return { assets: report, outcome: 'installed', toolResults, ...(await crossScope(plan, projectDir)) };
660
+ return {
661
+ assets: report,
662
+ ...targetsField(await readLockfile(projectDir), false),
663
+ outcome: 'installed',
664
+ toolResults,
665
+ ...(await crossScope(plan, projectDir)),
666
+ };
665
667
  });
666
- }, [adopt, cliOrg, dryRun, force, pin, specs.join(' '), tool]);
668
+ }, [adopt, all, cliOrg, dryRun, force, pin, specs.join(' '), tool]);
667
669
  if (state.status === 'loading') {
668
670
  return _jsx(Spinner, { message: state.message });
669
671
  }
@@ -686,10 +688,10 @@ function InstallAssets({ adopt, dryRun, force, json, org: cliOrg, pin, projectRo
686
688
  return renderJson({ ...data, ...fields });
687
689
  }
688
690
  if (data.outcome === 'dry-run-install') {
689
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: data.plan }), _jsx(NamedAssetLines, { assets: data.assets, dryRun: true, where: label }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
691
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'install', count: data.plan.totalCount, tools: data.tools, where: label }), _jsx(Text, { children: " " }), _jsx(PlanEntryLines, { plan: data.plan }), _jsx(NamedAssetLines, { assets: data.assets, dryRun: true, where: label }), _jsx(TargetLines, { bundles: data.bundles, dryRun: true, orgs: data.orgs, where: label }), _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
690
692
  }
691
693
  return (_jsxs(Box, { flexDirection: 'column', children: [data.outcome === 'installed' &&
692
- data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(NamedAssetLines, { assets: data.assets, where: label }), data.outcome === 'installed' && _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
694
+ data.toolResults.map(({ results, skipped, tool: toolName, warnings }) => (_jsx(InstallSummary, { results: results, skipped: skipped, tools: [toolName], warnings: warnings, where: label }, toolName))), _jsx(NamedAssetLines, { assets: data.assets, where: label }), _jsx(TargetLines, { bundles: data.bundles, orgs: data.orgs, where: label }), data.outcome === 'installed' && _jsx(CrossScopeWarning, { installs: data.crossScope ?? [] })] }));
693
695
  }
694
696
  /** `atk install <spec>` with the run's own flags, for a command an error suggests. */
695
697
  function installCommandFor(spec, { adopt, dryRun, force, pin, projectRoot, tool, user }) {
@@ -953,11 +955,6 @@ function InstallOrg({ cliOrg, dryRun, force, json, orgName, pin, projectRoot, sk
953
955
  }
954
956
  return null;
955
957
  }
956
- /** Whether a lockfile asset was installed as part of this bundle (name, org) at this version. */
957
- function isFromBundleVersion(installed, bundle, version) {
958
- const origin = installed.bundleOrigin;
959
- return origin?.name === bundle.name && origin.org === bundle.org && origin.version === version;
960
- }
961
958
  /** A member as a report names it: its spec and type. */
962
959
  function memberLabel({ name, org, type }) {
963
960
  return `${formatOrgName(org, name)} (${type})`;
@@ -986,8 +983,46 @@ function NamedAssetLines({ assets, dryRun = false, where, }) {
986
983
  ? `${dryRun ? 'would become' : 'is now'} a direct install${inScope}.`
987
984
  : `is already installed${inScope}.`] }, identityKey(a)))) }));
988
985
  }
986
+ /**
987
+ * The error for a bundle name that only another scope has, when no asset can claim the name
988
+ * either, instead of a misleading "asset not found".
989
+ */
990
+ function otherScopeBundleError(registry, name, inlineOrg) {
991
+ if (inlineOrg) {
992
+ return new Error(`Bundle '${name}' not found in org '${inlineOrg}'.`);
993
+ }
994
+ const orgScopes = (registry.bundles ?? []).filter((b) => b.name === name && b.org).map((b) => `@${b.org}/${name}`);
995
+ return new Error(`Bundle '${name}' not found in the registry.` +
996
+ (orgScopes.length > 0 ? ` Did you mean ${orgScopes.join(' or ')}?` : ''));
997
+ }
989
998
  /** A plan's entries, each with its type and version, its dependencies marked. */
990
999
  function PlanEntryLines({ plan }) {
991
1000
  return (_jsx(_Fragment, { children: plan.entries.map((entry) => (_jsxs(Text, { children: [' ', entry.reason === 'dependency' ? (_jsxs(Text, { color: 'cyan', children: [formatOrgName(entry.asset.org, entry.asset.name), ' ', _jsxs(Text, { dimColor: true, children: ["(", entry.asset.type, ") v", entry.version, " \u2014 dependency"] })] })) : (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: formatOrgName(entry.asset.org, entry.asset.name) }), _jsxs(Text, { dimColor: true, children: [' ', "(", entry.asset.type, ") v", entry.version] })] }))] }, identityKey(entry.asset)))) }));
992
1001
  }
1002
+ /**
1003
+ * The members not installed yet that the plan installs without the bundle's origin: named on their
1004
+ * own, or with another bundle named before it. A dry run reports them as an install would.
1005
+ */
1006
+ function plannedOutsideBundle(plan, lockfile, members, bundle) {
1007
+ return members.flatMap((member) => {
1008
+ if (findInstalledAsset(lockfile, member.name, member.type, member.org))
1009
+ return [];
1010
+ const entry = plan.entries.find((e) => identityKey(e.asset) === identityKey(member));
1011
+ const origin = entry?.bundleOrigin;
1012
+ if (!entry || (origin?.name === bundle.name && origin.org === bundle.org))
1013
+ return [];
1014
+ return [{ ...(origin ? { bundle: origin } : {}), ...member }];
1015
+ });
1016
+ }
1017
+ /**
1018
+ * The bundles and orgs a run of several names had among them: what it did with each bundle, the
1019
+ * bundle's members outside it, and how many assets each org brought.
1020
+ */
1021
+ function TargetLines({ bundles = [], dryRun = false, orgs = [], where, }) {
1022
+ const inScope = where ? ` in ${where}` : '';
1023
+ return (_jsxs(_Fragment, { children: [bundles.map((bundle) => {
1024
+ const spec = formatOrgName(bundle.org, bundle.name);
1025
+ return (_jsxs(Fragment, { children: [bundle.outcome === 'already-installed' ? (_jsxs(Text, { children: [_jsx(Text, { color: 'yellow', children: "!" }), " Bundle ", _jsx(Text, { bold: true, children: spec }), " is already fully installed (v", bundle.version, ")", inScope, "."] })) : (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: "\u2713" }), " Bundle ", _jsx(Text, { bold: true, children: spec }), " v", bundle.version, ' ', dryRun ? 'would be installed' : 'installed', inScope, "."] })), _jsx(BundleReportLines, { bundleName: spec, dryRun: dryRun, report: bundle })] }, spec));
1026
+ }), orgs.map(({ count, org }) => (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: 'magenta', children: ["Org: @", org] }), _jsxs(Text, { dimColor: true, children: [' ', "\u2014 all ", count, " asset", count !== 1 ? 's' : ''] })] }, org)))] }));
1027
+ }
993
1028
  //# sourceMappingURL=install.js.map